为啥示例不编译,也就是(co-、contra-和in-)方差如何工作?

Posted

技术标签:

【中文标题】为啥示例不编译,也就是(co-、contra-和in-)方差如何工作?【英文标题】:Why doesn't the example compile, aka how does (co-, contra-, and in-) variance work?为什么示例不编译,也就是(co-、contra-和in-)方差如何工作? 【发布时间】:2009-03-19 17:46:16 【问题描述】:

从this question 开始,有人可以用 Scala 解释以下内容吗:

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


我了解类型声明中 +TT 之间的区别(如果我使用 T 则编译) .但是,如何在不求助于创建 unparametrized 的情况下实际编写一个类型参数协变的类?如何确保只能使用 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 而不是val。以下是我想要的:

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

【问题讨论】:

因为var 是可设置的,而val 不是。这也是为什么 scala 的不可变集合是协变的,而可变集合不是。 在这种情况下可能会很有趣:scala-lang.org/old/node/129 【参考方案1】:

一般来说,协变 类型参数是允许随着类的子类型而变化的类型参数(或者,随着子类型而变化,因此使用“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的参数类型是BA 的返回类型是 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,它需要某种类型的方法参数,这(与访问器一起)意味着不变性。协方差适用于不可变数据,因为唯一可能的操作是访问器,它可以被赋予协变返回类型。

【讨论】:

这可以用简单的英语表述为 - 你可以将更简单的东西作为参数,你可以返回更复杂的东西? Java 编译器 (1.7.0) 无法编译“Object[] arr = new int[1];”而是给出错误消息:“java:需要不兼容的类型:java.lang.Object []找到:int []”。我认为您的意思是“Object[] arr = new Integer[1];”。 当你提到时,“这条规则的原因留给读者作为练习(提示:考虑不同的情况,因为函数是子类型的,就像我上面的数组示例)。”你能举几个例子吗? @perryzheng 每this,取trait Animaltrait Cow extends Animaldef iNeedACowHerder(herder: Cow =&gt; Unit, c: Cow) = herder(c)def iNeedAnAnimalHerder(herder: Animal =&gt; Unit, a: Animal) = herder(a)。然后,iNeedACowHerder( a: Animal =&gt; println("I can herd any animal, including cows") , new Cow ) 没问题,因为我们的牧牛人可以放牛,但是 iNeedAnAnimalHerder( c: Cow =&gt; println("I can herd only cows, not any animal") , new Animal ) 给出了编译错误,因为我们的牧牛人不能放牧所有的动物。 这是相关的并帮助我解决了差异:typelevel.org/blog/2016/02/04/variance-and-functors.html【参考方案2】:

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

【讨论】:

【参考方案3】:

请参阅Scala by example,第 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 

如果您觉得我不理解您的问题(很可能),请尝试在问题描述中添加更多解释/上下文,我会再试一次。

回应您的编辑:不可变插槽是完全不同的情况...* 微笑 * 我希望上面的示例有所帮助。

【讨论】:

我已经读过了;不幸的是,我(仍然)不明白我该怎么做我上面问的(即实际上在 T 中编写参数化类协变) 我删除了我的downmark,因为我意识到这有点苛刻。我应该在问题中明确说明我已经通过示例从 Scala 中读取了这些信息;我只是希望以“不太正式”的方式对其进行解释 @oxbow_lakes smile 我担心 Scala By Example 不太正式的解释。充其量,我们可以尝试使用具体的例子来工作…… 抱歉 - 我不希望我的插槽是可变的。我刚刚意识到问题是我声明了 var 而不是 val【参考方案4】:

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

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

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

【讨论】:

以上是关于为啥示例不编译,也就是(co-、contra-和in-)方差如何工作?的主要内容,如果未能解决你的问题,请参考以下文章

bzoj_2676_Contra

为啥这个不允许编译器执行的示例会导致使用 cmov 取消引用空指针?

java阅读程序。判断整数m,n的最后值的数值与含义? 我算是算了,可不知道为啥,就是编译错误。郁闷的头

常量和编译时评估 - 为啥要改变这种行为

C语言中学指针时*和&是相互补充的,为啥啊?

为啥从 constexpr 引用生成的汇编代码与 constexpr 指针不同?