为什么示例不编译,又如何(共同,反向和反向)方差有效?

时间:2009-03-19 17:46:16

标签: generics scala covariance contravariance

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

4 个答案:

答案 0 :(得分:296)

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

trait List[+A]

List[Int]List[AnyVal]的子类型,因为IntAnyVal的子类型。这意味着,当预期值为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的子类型(或相同类型),而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具有协变性,一切正常。

请注意,此技巧仅在我们返回List的实例时才有效,该实例专门用于特定于较少类型的B。如果您尝试使List变为可变,那么事情就会中断,因为您最终尝试将类型B的值分配给类型为A的变量,而编译器不允许这样做。每当你有可变性时,你需要一个某种类型的mutator,它需要一个特定类型的方法参数,它(与访问器一起)意味着不变性。协方差适用于不可变数据,因为唯一可能的操作是访问器,可以给出协变返回类型。

答案 1 :(得分:27)

@Daniel解释得非常好。但简而言之,如果允许的话:

  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有点难以理解,一些具体的例子会有所帮助。