为什么这个例子不能编译,也就是说(co-、 conta-和 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类?

切! 我用的是 var而不是 val。下面是我想要的:

class Slot[+T] (val some: T) {
}
32248 次浏览

请参阅 举例说明 Scala,第57页以上关于此的完整讨论。

如果我没有理解错你的评论,你需要重新阅读从第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 的例子有点难理解,一些具体的例子会有所帮助。

通常,协变体类型参数是一个允许随着类的子类型而变化的类型参数(或者,随着子类型的变化而变化,因此使用“ co-”前缀)。更具体地说:

trait List[+A]

List[Int]List[AnyVal]的一个子类型,因为 IntAnyVal的一个子类型。这意味着当需要 List[AnyVal]类型的值时,可以提供 List[Int]的实例。对于泛型来说,这确实是一种非常直观的工作方式,但结果表明,在存在可变数据的情况下使用它是不健全的(打破类型系统)。这就是为什么泛型在 Java 中是不变的。使用 Java 数组(错误地协变)的不合理性的简单例子:

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

我们只是将 String类型的值赋给 Integer[]类型的数组。出于显而易见的原因,这是个坏消息。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改变了错误的方向。有趣的是,我们可以通过在 A中使 List逆变来解决这个问题,但是返回类型 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,它需要具有某种类型的方法参数,这(与访问器一起)意味着不变性。协方差适用于不可变数据,因为唯一可能的操作是访问器,它可能被赋予一个协变返回值类型。

@ 丹尼尔已经解释得很清楚了,但简而言之,如果允许的话:

  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 集合都是不变的。