为什么这个例子没有编译,又如何(合作,对立和in)方差工作?

继这个问题之后 ,有人可以在Scala中解释以下内容:

class Slot[+T] (var some: T) { // DOES NOT COMPILE // "COVARIANT parameter in CONTRAVARIANT position" } 

我理解类型声明中的+TT之间的区别(如果我使用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 } } 

这一切都很好,但我现在有两个类型参数,我只想要一个。 我会重新问这个问题:

我怎样才能编写一个不可变的 Slot类,它的类型是协变的

编辑2 :杜! 我用var和not val 。 以下是我想要的:

 class Slot[+T] (val some: T) { } 

一般来说, 协变类型参数是允许随着类别分类而变化的参数(或者根据子类型,因此是“共同”前缀而变化)。 更具体地说:

 trait List[+A] 

List[Int]List[AnyVal]的子类型,因为IntAnyVal的子类型。 这意味着当需要List[AnyVal]类型的值时,您可以提供List[Int]的实例。 这对于泛型是非常直观的工作方式,但是在存在可变数据的情况下,这种方法是不健全的(打破了类型系统)。 这就是泛型在Java中不变的原因。 使用Java数组(这是错误的协变)的不完善性的简单例子:

 Object[] arr = new Integer[1]; arr[0] = "Hello, there!"; 

我们只是给类型为Integer[]的数组赋了一个String类型的值。 由于原因很明显,这是一个坏消息。 Java的类型系统实际上是在编译时允许的。 JVM将在运行时“帮助”抛出ArrayStoreException 。 Scala的类型系统可以防止这个问题,因为Array类的类型参数是不变的(声明是[A]而不是[+A] )。

请注意,还有另一种称为逆变的方差。 这是非常重要的,因为它解释了为什么协方差会导致一些问题。 逆向变换在字面上与协方差相反:参数随着子类型而向上变化。 部分原因是它不那么常见,因为它是非常直观的,虽然它有一个非常重要的应用程序:函数。

 trait Function1[-P, +R] { def apply(p: P): R } 

注意P型参数上的“ ”方差注释。 这个声明作为一个整体意味着Function1P是逆变的,在R是协变的。 因此,我们可以推导出以下公理:

 T1' <: T1 T2 <: T2' ---------------------------------------- S-Fun Function1[T1, T2] <: Function1[T1', T2'] 

请注意, T1'必须是T1'的子类型(或相同类型),而T2T2'则相反。 在英语中,这可以被理解为以下内容:

如果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是协变的,一切都可以解决。

注意,这个技巧只有在我们返回一个专门用于较少特定类型BList的实例时才有效。 如果你试图让List变为可变的,那么事情就会崩溃,因为你最终试图将类型B值赋给A编译器不允许的变量。 每当你有可变性时,你需要有一个某种类型的增变器,它需要一个特定类型的方法参数,它和访问器一起意味着不变性。 协变性与不可变数据一起工作,因为唯一可能的操作是一个访问器,可以给它一个协变返回类型。

丹尼尔已经解释得很好。 但是总而言之,如果允许:

  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 (Duh!)时不成功。

一般来说,可变性与协方差和反方差不一致。 这就是为什么所有Java集合都是不变的原因。

请参阅第57页上的Scala示例 ,以获得对此的完整讨论。

如果我正确地理解了你的评论,你需要重新阅读从第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 

如果你觉得我不理解你的问题(一个明显的可能性),尝试添加更多的解释/上下文的问题描述,我会再试一次。

回应你的编辑:不变的插槽是一个完全不同的情况… *微笑*我希望上面的例子帮助。

您需要对参数应用下限。 我很难记住语法,但我认为它看起来像这样:

 class Slot[+T, V <: T](var some: V) { //blah } 

Scala的例子有点难以理解,一些具体的例子会有所帮助。