协变与逆变
第一次接触 covariant(协变) 和 contravariant(逆变) 是在学习 Rust 的时候,然后就因为没看懂放弃了。直到最近有一次看到了这个术语,结合了 Kotlin 中的 covariant 和 contravariant ,逐渐理解了 Rust 中的 covariant 和 contravariant。下面我们从 Kotlin 中的 covariant 和 contravariant 入手,然后将其推广到 Rust 中。
现在我们有三个 class: Person
、Student
、Teacher
,其中 Student
、Teacher
均继承自 Person
open class Person(var age: Int = 1) {}
class Student(): Person() {}
class Teacher(): Person() {}
Covariant
如果一个类型 Child
是另一个类型 Parent
的子类型,那么对于 类型 T
, T<Child>
也是 T<Parent>
的子类型, 我们就说T
在泛型参数上是 covariant 的即:如果 Child
是 Parent
的子类型,那么 T<Child>
也是 T<Parent>
的子类型
fun copy(from: Array<Person>, to: Array<Person>) { }
fun main() {
var workers: Array<Student> = arrayOf<Student>(Student())
var workerCollection: Array<Person> = Array<Person>(1){ Person() };
copy(workers, workerCollection)
}
在 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,这里我们采用后者。
class Array<out T>
// or
fun copy(from: Array<out Person>, to: Array<out Person>) { }
现在我们来完善这个方法:
fun copy(from: Array<out Person>, to: Array<out Person>) {
assert(from.size == to.size)
for (i in from.indices) {
// to[i] = from[i] // compile error
}
}
如果 from
的泛型参数为 Student
,而 to
的泛型参数为 Person
,就涉及到了将父类型赋值给子类型的可能,这是类型不安全的。我们甚至还可以这样调用:
var workers: Array<Student> = arrayOf<Student>(Student())
var workerCollection: Array<Teacher> = Array<Teacher>(1){ Teacher() };
copy(workers, workerCollection)
这样调用是 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
,都应该被添加进来
fun collect(): Array<Student> {
return arrayOf<Student>(Student())
}
fun collectContravariance(): Array< Student> {
return arrayOf<Person>(Person()) // compile error
}
collect
可以正常工作,因为类型是匹配的。但是 collectContravariance
会报错💣,因为 Array
是 invariant,即既不是 contravariant 也不是 covariant 的。
为了让其正确工作,我们必须让其是 contravariant 的,因为我们可能会将 Person
父类型赋值给 Student
子类型
fun collectContravariance(): Array<in Student> {
return arrayOf<Person>(Person()) // OK
}
我们甚至可以这样写:
fun collectContravariance(): Array<in Student> {
return arrayOf<Person>(Teacher()) // OK
}
但是如果我们想要访问返回值的 age
属性,编译器就会报错💣,这说明 contravariant 是不可读的。
fun main() {
var contravarianceStudents = collectContravariance()
var students = collect()
students[0].age = 1 // ok
println(students[0].age) // ok
contravarianceStudents[0] = Student() // ok
println(contravarianceStudents[0].age) // compile error
}
但是我们仍然可以对其进行修改,说明 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 的概念,不过与直觉不同的是,生命周期长的类型是生命周期短的类型的子类型
long
|- short shorter
| |- _
| |_ |_
|_
long <: short <: shorter
在 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)
fn debug<'a>(a: &'a str, b: &'a str) {
println!("a = {a:?} b = {b:?}");
}
fn main() {
let hello: &'static str = "hello";
{
let world = String::from("world");
debug(hello, &world);
}
}
需要注意的是 &'a mut T
对于 'a
是 covariant,对于 T
是 invariant 的。所以下面的代码是不正确的(因为 &'a mut T
对于 T
是 invariant 的,这意味着传入的参数必须与函数签名相同,即 &'world str
)
fn assign<T>(input: &mut T, val: T) {
*input = val;
}
fn main() {
let mut hello: &'static str = "hello";
{
let world = String::from("world");
assign(&mut hello, &world); // compile error
}
println!("{hello}"); // use after free 😿
}
我们改造一下:
fn main() {
let mut hello: & str = "hello";
{
let mut world = String::from("world");
assign(&mut hello, &world);
println!("{hello}");
}
// println!("{hello}"); // use after free 😿
}
这次可以正确运行了。这是因为 &mut hello
的生命周期可以与 &world
相同,因为在 world
销毁后就没有使用 hello
的地方了。如果我们在最后一行使用了 hello
,那么上述代码就会报错,因为 &'a mut T
对于 T
是 invariant 的,而 &'hello str
与 &'world str
类型不相同。
注意:生命周期并不与作用域等价,即生命周期可以与作用域相同,也可以与作用域不同
第二个需要注意的点是函数指针,就 fn(T) -> U
对 T 是 contravariant 的,对 U 是 covariant 的。