![Rust中的子类型与变体](/_astro/subtype.XVfWgj03_pkXF7.webp)
Rust中的子类型与变体
协变,逆变和不变
![Avatar](/_astro/_avatar.B-jQIxXa_ZRo5Hk.webp)
众所周知,在Rust当中,生命周期标注是属于类型系统的一部分。但是总有些时候,生命周期标注总是出问题。无论如何与编译器搏斗,你的生命周期标注总是不对。这时候一般就是对协变的理解出现了问题。
Rust的协变机制可以总结为下面这张表(来自The Rustonomicon)
‘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 |
看起来非常的复杂,而且难以理解。但是,事实上我们可以有比较容易的技巧来理解记住他们
子类型(Subtyping)
在理解那些奇奇怪怪的可变性之前。我们需要先理解子类型。
那么,什么是子类型呢?
简单来说,假设我们有两种类型A,B。若我们在可以用A的所有场合都可以用B来代替。那么我们就可以简单的认为B是A的子类型。
在Java当中,我们知道,除了primitive type之外的所有类型都是继承于Object这个类。当我们使用Object这个类的过程当中。我们可以用任何类型去替代。比如
public static foo(Object obj) {
System.out.println(obj.toString());
}
String str = "This is a string!";
// 这里用子类型替代了父类型
foo(str);
Java的这种树形继承的方式是我们最熟悉的一种方式。我们还给他起了一个名字来形容他:向上转型。用来形容这种子类型代替父类型所形成的一种多态机制
但是,当我们用这种类型系统去描述生命周期这种现象的时候,奇怪的事情发生了
当我们使用带有生命周期的变量/资源的过程中。我们都一个原则:当一个函数,或者任何一段代码。对一个变量的生命周期的有效期要求越短,那么他的要求越宽松。换言之,如果对一个变量的生命周期有效期要求越长,那么他的要求越严格。
'a: |--------|
'b: |-------|
'c: |--------------|
'static: |----------------------------------|
我们可以把生命周期看作一段段区间。我们很清晰的看到。‘static这个生命周期是最长的,他和整个程序的生命周期一样长。‘a和’b错位。而’a可以看成是’c的子区间。那么,在’a这个区间内,‘c是一直有效的。我们可以说,‘c就是’a的子类型。
我们可以看到。‘static在以上所有的生命周期内都有效。事实上,‘static在任何生命周期内都有效,因为没有任何其他人比它更长。所以,‘static比任何生命周期都严格,我们可以说。‘static可以是任何生命周期的子类型。这确实比较反直觉,我们原先习惯那种自上而下的,单个父亲,多个孩子的树形的类型结构,但是这个更像是反过来,多个父亲,单个孩子。
协变 (covariant)
了解了子类型之后,我们就来聊聊这些乱七八糟的可变性了。协变这里的一个比较粗野的理解就是,当一个类型A是另一个类型B的子类型,那么当他们都被一个东西包裹起来之后,Wrapper(A)还是Wrapper(B)的子类型。
对于类似Box<T>
和Vec<T>
这些都比较好理解,当这里的T是U的子类型,那么Box<T>
和Vec<T>
理所应当是Box<U>
和Vec<U>
的子类型。
// B是A的子类型
fn foo_a(Vec<A> a) { }
fn foo_b(Vec<B> b) { }
Vec<A> a = vec![];
Vec<B> b = vec![];
// 这两个都是可以的
foo_a(a);
foo_a(b);
// 这个不可以
foo_b(a);
foo_b(b);
对于借用,对于生命周期来说,很自然,一个比较长的生命周期是一个比较短的生命周期的子类型,那么有着比较长的生命周期的借用自然可以用在比较短的生命周期的借用上。对于闭包。来说,对于返回值类型,我有一个返回子类型的闭包自然可以用在返回父类型的闭包上。因为当我们调用这个闭包的时候,我们可以安全并且自然而然的把返回值变成父类型。
fn some_high_order_func(foo: Fn() -> &'a str) {
let f = foo();
}
let some_string = new String("str").as_str();
// 这样都是可以的
some_high_order_func(() -> "str");
some_high_order_func(() -> some_string);
逆变 (contravariant)
在Rust当中,只有一种类型是逆变的,也就是闭包的参数。但是这个也很好理解:当我有一个参数,并且我想调用传进来的闭包的时候。我们希望对参数的要求越宽松越好。
// B是A的子类型
fn foo_a(A a) -> ()
// 但不能传进这个函数
fn foo_b(B b) -> ()
A a = A::new();
B b = B::new();
// 两个情况都可以处理
foo_a(b);
foo_a(a);
// 只能处理一种情况
foo_b(b);
// 这里会出问题
foo_b(a);
这里,foo_a
可以处理两种情况,foo_b
只能处理一种情况。原本B是子类型,B可以处理更多的情况,但是变成参数时,这个函数foo_b
反而只能处理更少的情况。我们就可以说。这里发生了逆变。也就是包装过后子类型颠倒了。
不变性
不变性可以理解成,包装之后好,类型必须完全匹配。这里我们发现,所有的不变形都是包装是可变的。它可以让外部修改内部的值。比如&'a mut T
,UnsafeCell<T>
,Cell<T>
和*mut T
。我们可以理解为,外部包装可变会导致协变性不能传导到内部。而那些依然有着协变性的。都是只提供的不可变借用。