从this question开始,有人可以在Scala中解释以下内容:
class Slot[+T] (var some: T) {
// DOES NOT COMPILE
// "COVARIANT parameter in CONTRAVARIANT position"
}
我理解类型声明中 +T
和 T
之间的区别(如果我使用 {{1},它会编译} )。但是,如何在不诉诸创建 unparametrized 的东西的情况下,如何实际编写一个在其类型参数中具有协变性的类?如何确保只能使用 T
的实例创建以下内容?
T
编辑 - 现在将其归结为以下内容:
class Slot[+T] (var some: Object){
def get() = { some.asInstanceOf[T] }
}
这一切都很好,但我现在有两个类型参数,我只想要一个。我会再问这个问题:
如何在类型中编写不可变的 abstract class _Slot[+T, V <: T] (var some: V) {
def getT() = { some }
}
类协变?
编辑2 :呃!我使用的是Slot
而不是var
。以下是我想要的:
val
答案 0 :(得分:296)
通常,协变类型参数是允许在类被子类型化时变化的参数(或者,随着子类型而变化,因此“co-”前缀)。更具体地说:
trait List[+A]
List[Int]
是List[AnyVal]
的子类型,因为Int
是AnyVal
的子类型。这意味着,当预期值为List[Int]
的值时,您可以提供List[AnyVal]
的实例。这对于泛型工作来说确实是一种非常直观的方式,但事实证明,当存在可变数据时,它是不合理的(打破类型系统)。这就是泛型在Java中不变的原因。使用Java数组(错误协变)的不健全的简要示例:
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
我们刚刚将类型String
的值分配给类型为Integer[]
的数组。由于显而易见的原因,这是个坏消息。 Java的类型系统实际上允许在编译时使用它。 JVM将“帮助”在运行时抛出ArrayStoreException
。 Scala的类型系统可以防止出现此问题,因为Array
类上的类型参数是不变的(声明为[A]
而不是[+A]
)。
请注意,还有另一种称为 contravariance 的方差。这非常重要,因为它解释了为什么协方差会导致一些问题。逆变性实际上与协方差相反:参数随着子类型而变化向上。虽然它确实有一个非常重要的应用程序:函数,但是它不太常见,因为它非常直观。
trait Function1[-P, +R] {
def apply(p: P): R
}
请注意P
类型参数上的“ - ”方差注释。整个声明意味着Function1
中的P
是逆变的,R
中的变量是逆变的。因此,我们可以推导出以下公理:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
请注意,T1'
必须是T1
的子类型(或相同类型),而T2
和T2'
则相反。在英语中,这可以理解为:
函数 A 是另一个函数 B 的子类型,如果 A 的参数类型是 B ,而 A 的返回类型是返回类型 B 的子类型。
这个规则的原因留给了读者一个练习(提示:考虑不同的情况,因为函数是子类型的,就像我上面的数组例子一样)。
通过您对共同和逆变的新发现的知识,您应该能够看到以下示例无法编译的原因:
trait List[+A] {
def cons(hd: A): List[A]
}
问题是A
是协变的,而cons
函数期望其类型参数是不变的。因此,A
改变了错误的方向。有趣的是,我们可以通过在List
中使用A
逆变来解决此问题,但是返回类型List[A]
将无效,因为cons
函数期望其返回类型为协变
我们这里唯一的两个选项是a)使A
不变,丢失协方差的漂亮,直观的子类型属性,或者b)向定义的cons
方法添加本地类型参数A
作为下限:
def cons[B >: A](v: B): List[B]
现在有效。您可以想象A
向下变化,但B
相对于A
能够向上变化,因为A
是其下限。使用此方法声明,我们可以使A
具有协变性,一切正常。
请注意,此技巧仅在我们返回List
的实例时才有效,该实例专门用于特定于较少类型的B
。如果您尝试使List
变为可变,那么事情就会中断,因为您最终尝试将类型B
的值分配给类型为A
的变量,而编译器不允许这样做。每当你有可变性时,你需要一个某种类型的mutator,它需要一个特定类型的方法参数,它(与访问器一起)意味着不变性。协方差适用于不可变数据,因为唯一可能的操作是访问器,可以给出协变返回类型。
答案 1 :(得分:27)
class Slot[+T](var some: T) {
def get: T = some
}
val slot: Slot[Dog] = new Slot[Dog](new Dog)
val slot2: Slot[Animal] = slot //because of co-variance
slot2.some = new Animal //legal as some is a var
slot.get ??
然后 slot.get
会在运行时抛出错误,因为它未能将Animal
转换为Dog
(呃!)。
一般来说,变异性与协方差和反方差不相符。这就是为什么所有Java集合都是不变的原因。
答案 2 :(得分:7)
有关此问题的完整讨论,请参阅第57页的Scala by example。
如果我正确理解你的评论,你需要重读从第56页底部开始的段落(基本上,我认为你要求的是没有运行时检查的类型安全,scala没有'你这么做,所以你运气不好。翻译他们的例子以使用你的构造:
val x = new Slot[String]("test") // Make a slot
val y: Slot[Any] = x // Ok, 'cause String is a subtype of Any
y.set(new Rational(1, 2)) // Works, but now x.get() will blow up
如果您觉得我不理解您的问题(一种明显的可能性),请尝试在问题描述中添加更多解释/上下文,我会再试一次。
回应你的编辑:不可变的插槽是完全不同的情况...... *微笑*我希望上面的例子有所帮助。
答案 3 :(得分:3)
您需要在参数上应用下限。我很难记住语法,但我认为它看起来像这样:
class Slot[+T, V <: T](var some: V) {
//blah
}
Scala-by-example有点难以理解,一些具体的例子会有所帮助。