为什么这个例子没有编译,又如何(合作,对立和in)方差工作?
继这个问题之后 ,有人可以在Scala中解释以下内容:
class Slot[+T] (var some: T) { // DOES NOT COMPILE // "COVARIANT parameter in CONTRAVARIANT position" }
我理解类型声明中的+T
和T
之间的区别(如果我使用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]
的子类型,因为Int
是AnyVal
的子类型。 这意味着当需要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
型参数上的“ – ”方差注释。 这个声明作为一个整体意味着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
正在改变错误的方向。 有趣的是,我们可以通过使List
逆转为A
来解决这个问题,但是返回类型List[A]
将是无效的,因为cons
函数期望它的返回类型是协变的 。
我们在这里唯一的两个选择是a)使A
不变的,失去了协变性的好的,直观的子类型的属性,或者b)将一个局部类型参数添加到cons
方法中,将A
定义为一个下界:
def cons[B >: A](v: B): List[B]
这现在是有效的。 你可以想象, A
是向下变化的,但是B
相对于A
可以向上变化,因为A
是下限。 通过这个方法声明,我们可以使A
是协变的,一切都可以解决。
注意,这个技巧只有在我们返回一个专门用于较少特定类型B
的List
的实例时才有效。 如果你试图让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的例子有点难以理解,一些具体的例子会有所帮助。