第一次接触 covariant(协变) 和 contravariant(逆变) 是在学习 Rust 的时候,然后就因为没看懂放弃了。直到最近有一次看到了这个术语,结合了 Kotlin 中的 covariant 和 contravariant ,逐渐理解了 Rust 中的 covariant 和 contravariant。下面我们从 Kotlin 中的 covariant 和 contravariant 入手,然后将其推广到 Rust 中。

现在我们有三个 class: PersonStudentTeacher,其中 StudentTeacher 均继承自 Person

1
2
3
4
5
open class Person(var age: Int = 1) {}  

class Student(): Person() {}

class Teacher(): Person() {}

Covariant

如果一个类型 Child 是另一个类型 Parent 的子类型,那么对于 类型 TT<Child> 也是 T<Parent> 的子类型, 我们就说T 在泛型参数上是 covariant 的即:如果 ChildParent 的子类型,那么 T<Child> 也是 T<Parent> 的子类型

1
2
3
4
5
6
7
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 的,所以认为这是类型不安全的,这种情况我们称 Arrayinvariant 的。

注:Array 可以替换为 whatever 其他 invariant 类型

为了让 Array 是 covariant 的,在 Kotlin 中需要添加 out 标识。我们可以在 Array 类型中标识,也可以在方法声明中声明我们接收的泛型参数是 covariant,这里我们采用后者。

1
2
3
class Array<out T>
// or
fun copy(from: Array<out Person>, to: Array<out Person>) { }

现在我们来完善这个方法:

1
2
3
4
5
6
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,就涉及到了将父类型赋值给子类型的可能,这是类型不安全的。我们甚至还可以这样调用:

1
2
3
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 的子类型,那么对于 类型 TT<Parent>T<Child> 的子类型, 我们就说T 在泛型参数上是 contravariant 的即:如果 ChildParent 的子类型,那么 T<Parent>T<Child> 的子类型

contravariant 可能有点反直觉,但是考虑这样一个函数:我们认为所有 Person 都可以是 Student,并且我们需要收集这些 Person。那么不管他是 Student 还是 Teacher,都应该被添加进来

1
2
3
4
5
6
7
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 子类型

1
2
3
fun collectContravariance(): Array<in Student> {  
return arrayOf<Person>(Person()) // OK
}

我们甚至可以这样写:

1
2
3
fun collectContravariance(): Array<in Student> {  
return arrayOf<Person>(Teacher()) // OK
}

但是如果我们想要访问返回值的 age 属性,编译器就会报错💣,这说明 contravariant 是不可读的。

1
2
3
4
5
6
7
8
9
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 by Sub.
  • '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
2
3
4
5
6
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 对于 'aT 都是 covariant 的,这很好理解,因为它是只读的。所以下面的代码是正确的(因为 &'static <: 'a 可以推导出 `&static str <: &'a str)

1
2
3
4
5
6
7
8
9
10
11
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)

1
2
3
4
5
6
7
8
9
10
11
12
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 😿
}

我们改造一下:

1
2
3
4
5
6
7
8
9
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 的。