协变与逆变
第一次接触 covariant(协变) 和 contravariant(逆变) 是在学习 Rust 的时候,然后就因为没看懂放弃了。直到最近有一次看到了这个术语,结合了 Kotlin 中的 covariant 和 contravariant ,逐渐理解了 Rust 中的 covariant 和 contravariant。下面我们从 Kotlin 中的 covariant 和 contravariant 入手,然后将其推广到 Rust 中。
现在我们有三个 class: Person、Student、Teacher,其中 Student、Teacher 均继承自 Person
1 | open class Person(var age: Int = 1) {} |
Covariant
如果一个类型 Child 是另一个类型 Parent 的子类型,那么对于 类型 T, T<Child> 也是 T<Parent> 的子类型, 我们就说T 在泛型参数上是 covariant 的即:如果 Child 是 Parent 的子类型,那么 T<Child> 也是 T<Parent> 的子类型
1 | fun copy(from: Array<Person>, to: Array<Person>) { } |
在 copy(from: Array<out Person>, to: Array<Person>) 这个例子中,我们接收两个参数,每个参数都是 Array<Person> 类型的,然后我们调用该方法是分别传入了 Array<Student> 和 Array<Person> 类型的参数。乍一看好像没什么问题,但是编译器会报错,因为编译器并不知道 Array 是 covariant 的,所以认为这是类型不安全的,这种情况我们称 Array 是 invariant 的。
注:
Array可以替换为 whatever 其他 invariant 类型
为了让 Array 是 covariant 的,在 Kotlin 中需要添加 out 标识。我们可以在 Array 类型中标识,也可以在方法声明中声明我们接收的泛型参数是 covariant,这里我们采用后者。
1 | class Array<out T> |
现在我们来完善这个方法:
1 | fun copy(from: Array<out Person>, to: Array<out Person>) { |
如果 from 的泛型参数为 Student,而 to 的泛型参数为 Person,就涉及到了将父类型赋值给子类型的可能,这是类型不安全的。我们甚至还可以这样调用:
1 | var workers: Array<Student> = arrayOf<Student>(Student()) |
这样调用是 OK 的,因为这符合 covariant 的规定。但是如果我们在 copy 中进行了写操作,那么就会爆炸💥,因为无法保证类型是安全的。所以 covariant 是只读的。
Contravariant
如果一个类型 Child 是另一个类型 Parent 的子类型,那么对于 类型 T, T<Parent> 是 T<Child> 的子类型, 我们就说T 在泛型参数上是 contravariant 的即:如果 Child 是 Parent 的子类型,那么 T<Parent> 是 T<Child> 的子类型
contravariant 可能有点反直觉,但是考虑这样一个函数:我们认为所有 Person 都可以是 Student,并且我们需要收集这些 Person。那么不管他是 Student 还是 Teacher,都应该被添加进来
1 | fun collect(): Array<Student> { |
collect 可以正常工作,因为类型是匹配的。但是 collectContravariance 会报错💣,因为 Array 是 invariant,即既不是 contravariant 也不是 covariant 的。
为了让其正确工作,我们必须让其是 contravariant 的,因为我们可能会将 Person 父类型赋值给 Student 子类型
1 | fun collectContravariance(): Array<in Student> { |
我们甚至可以这样写:
1 | fun collectContravariance(): Array<in Student> { |
但是如果我们想要访问返回值的 age 属性,编译器就会报错💣,这说明 contravariant 是不可读的。
1 | fun main() { |
但是我们仍然可以对其进行修改,说明 contravariant 是可写的。
Rust
你以为这样就完了吗?注意,我们上面的讨论都是基于 ”类型“ 进行讨论的,所以 covariant, invariant, contravariant 的概念适用于所有类型,甚至也适用于非类型。
在 the essennce of algol 里面,一个 variable 被拆分为 “读” 和 “写” 两个部分,其中 “读” 的部分是 covariant,”写“的部分是 contravariant,可读可写的则是 invariant。
现在我们来看看 Rust 中是如何相关概念的
在 死灵书中有如下几个定义:
- Subtyping is the idea that one type can be used in place of another.
- the set of requirements that
Superdefines are completely satisfied bySub.'adefines a region of code'long <: 'shortif and only if'longdefines a region of code that completely contains'short
我们可以看到,Rust 中的生命周期也符合 covariant, invariant, contravariant 的概念,不过与直觉不同的是,生命周期长的类型是生命周期短的类型的子类型
1 | long |
在 class 中试图将 父类型赋值给子类型是危险的,与此类似,试图扩张生命周期是危险的(比如将 short 扩张为 long,这个行为是危险的)。
下面是 Rust 中 泛型类型与 variance 之间的关系
| 'a | T | U | |
|---|---|---|---|
&'a T |
covariant | covariant | |
&'a mut T |
covariant | invariant | |
Box<T> |
covariant | ||
Vec<T> |
covariant | ||
UnsafeCell<T> |
invariant | ||
Cell<T> |
invariant | ||
fn(T) -> U |
contravariant | covariant | |
*const T |
covariant | ||
*mut T |
invariant |
&'a T 对于 'a 和 T 都是 covariant 的,这很好理解,因为它是只读的。所以下面的代码是正确的(因为 &'static <: 'a 可以推导出 `&static str <: &'a str)
1 | fn debug<'a>(a: &'a str, b: &'a str) { |
需要注意的是 &'a mut T 对于 'a 是 covariant,对于 T 是 invariant 的。所以下面的代码是不正确的(因为 &'a mut T 对于 T 是 invariant 的,这意味着传入的参数必须与函数签名相同,即 &'world str)
1 | fn assign<T>(input: &mut T, val: T) { |
我们改造一下:
1 | fn main() { |
这次可以正确运行了。这是因为 &mut hello 的生命周期可以与 &world 相同,因为在 world 销毁后就没有使用 hello 的地方了。如果我们在最后一行使用了 hello,那么上述代码就会报错,因为 &'a mut T 对于 T 是 invariant 的,而 &'hello str 与 &'world str 类型不相同。
注意:生命周期并不与作用域等价,即生命周期可以与作用域相同,也可以与作用域不同
第二个需要注意的点是函数指针,就 fn(T) -> U 对 T 是 contravariant 的,对 U 是 covariant 的。

