通常,协变类型参数是允许随着类被子类型化而向下变化的类型参数(或者,随着子类型而变化,因此使用“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 的值分配给 typeString
的数组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
正在改变错误的方向。有趣的是,我们可以通过使List
in 逆变来解决这个问题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
可变,那么事情就会崩溃,因为您最终尝试将 type 的值分配给 typeB
的变量A
,这是编译器不允许的。每当您具有可变性时,您就需要某种类型的mutator,它需要某种类型的方法参数,这(与访问器一起)意味着不变性。协变适用于不可变数据,因为唯一可能的操作是访问器,它可以被赋予协变返回类型。