协变与逆变
第一次接触 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
Super
defines are completely satisfied bySub
.'a
defines a region of code'long <: 'short
if and only if'long
defines 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 的。