通常,协变类型参数是一个允许随类的子类型而变化的参数(或者,随子类型而变化,因此有“ co-”前缀)。更具体地说:
trait List[+A]
List[Int]
是的子类型,List[AnyVal]
因为Int
是的子类型AnyVal
。这意味着您可以提供List[Int]
何时需要类型值的实例List[AnyVal]
。这实际上是泛型工作的非常直观的方式,但事实证明,在存在可变数据的情况下使用泛型是不合理的(破坏类型系统)。这就是为什么泛型在Java中是不变的。使用Java数组(错误协变量)的不健全的简要示例:
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
我们只是将type的值分配给type String
的数组Integer[]
。出于显而易见的原因,这是一个坏消息。Java的类型系统实际上在编译时就允许这样做。JVM将ArrayStoreException
在运行时“有帮助”地抛出一个。Scala的类型系统避免了此问题,因为类上的类型参数Array
是不变的(声明[A]
不是[+A]
)。
请注意,还有另一种类型的方差,称为convariance。这非常重要,因为它说明了为什么协方差会导致某些问题。字面上的相反是协方差:参数随着子类型的变化而向上变化。尽管它确实有一个非常重要的应用程序:函数,但它却不那么直观,部分原因是它是如此违反直觉。
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
in 成为变量来解决此问题A
,但是List[A]
由于cons
函数期望其返回类型为covariant,因此返回类型将无效。
我们这里仅有的两个选择是:a)使A
不变,失去协方差的漂亮,直观的子类型属性,或b)向cons
定义A
为下限的方法中添加局部类型参数:
def cons[B >: A](v: B): List[B]
现在有效。您可以想象它A
是向下变化的,但是由于它的下限,B
因此能够相对向上变化。使用此方法声明,我们可以是协变的,一切都可以进行。A
A
A
注意,只有当我们返回List
专门针对次特定类型的实例时,此技巧才有效B
。如果尝试使变量List
可变,则由于最终尝试将type的值分配给type B
的变量而A
导致编译失败,编译器不允许这样做。只要具有可变性,就需要具有某种可变器,该可变器需要某种类型的方法参数(与访问器一起使用)意味着不变性。协方差适用于不可变数据,因为唯一可能的操作是访问器,可以为访问器指定协变返回类型。
var
可设定而val
不是。这也是为什么scala的不可变集合是协变的,而可变的集合却不是。