为啥示例不编译,也就是(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"
我了解类型声明中 +T
和 T
之间的区别(如果我使用 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]
的子类型,因为Int
是AnyVal
的子类型。这意味着当需要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
类型参数上的“-”差异注释。这个声明作为一个整体意味着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
改变了错误的方向。有趣的是,我们可以通过使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 Animal
、trait Cow extends Animal
、def iNeedACowHerder(herder: Cow => Unit, c: Cow) = herder(c)
和def iNeedAnAnimalHerder(herder: Animal => Unit, a: Animal) = herder(a)
。然后,iNeedACowHerder( a: Animal => println("I can herd any animal, including cows") , new Cow )
没问题,因为我们的牧牛人可以放牛,但是 iNeedAnAnimalHerder( c: Cow => 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-)方差如何工作?的主要内容,如果未能解决你的问题,请参考以下文章
为啥这个不允许编译器执行的示例会导致使用 cmov 取消引用空指针?