从 variance 角度理解 Rust 中的生命周期从 variance 角度理解 Rust 中的生命周期, 并且结合
本文将从 variance 角度理解 Rust 中的生命周期, 并且结合 TypeScript 解释 variance, 适用于有一定 TypeScript 背景的 Rust 初学者.
什么是 variance
variance (变形) 是编程语言类型系统中的一个通用概念, 广泛存在于各种语言的类型系统中, 用于描述如果一个类型 A 和类型 B 具有父子关系, 那么将一个 A 放在需要 B 的位置的可行性.
如果 B 是 A 的子类型, 那么我们可以说 B 比 A 更有用(至少和 A 一样有用), 因为 A 有的功能 B 全都有. 所以一般情况下, 在顺着数据流向的前提下, 需要 A 的位置, 我们都可以放一个 B 上去. 而 variance 是将数据流向隐藏起来之后, 对是否可以将 B 放在 A 的位置的一种结论性总结.
variance 一般可分为下面三类
- covariance (协变)
- invariance (不变)
- contravariance (逆变)
关于 variance 的描述一般为: 类型 T
中, 对于类型 U
是 covariant/invariant/contravariant. 其中 T
是主要关注的类型, U
是在 T
中出现的类型. 比如下列描述 (TypeScript):
T
对于T
是 covariantT[]
对于T
是 covariant(item: T) => void
对于T
是 contravariant
请注意这非常重要, 说明了 variance 是一个类型与它内部的某一部分类型的关系.
由于 Rust 中没有继承, 绝大多数类型不存在父子关系, 接下来我会使用 TypeScript 举例子说明, 并且开启 tsconfig.json
中的 strict
. 我会重点说明 covariance 和 invariance, 因为这两者对于理解生命周期至关重要. 最后我会简单介绍一下 contravariance.
Covariance
Covariance (协变), 指的是对于类型 Parent 和 Child, 如果一个位置需要 Parent, 那么可以将 Child 放上去. 以 TypeScript 为例:
class Animal {
name = "";
hello() {
}
}
class Cat extends Animal {
catchJerry() {
}
}
function say(animal: Animal) {
person.hello();
}
let cat = new Cat();
say(cat);
say
函数接收一个类型为 Animal
的参数, 但是在调用的时候传入一个 Cat
类型的参数也可行, 因为 Cat
是 Animal
的子类型, Animal
所拥有的属性和方法 Cat
全都拥有, 所以 Cat
比 Animal
更有用. 在函数调用的场景里, 如果我们传入的类型比函数需要的类型更有用, 那自然是可行的. 这时候我们可以说, Animal
类型对于 Animal
是 covariant.
Invariance
Invariance (不变), 指的是对于类型 Parent 和 Child, 如果一个位置需要 Parent, 那么只能将 Parent 放上去, 不可以将 Child 放上去. 如果一个位置需要 Child, 那么只能将 Child 放上去, 不能将 Parent 放上去
因为我没有找到 TypeScript 中 invariance 的例子, 所以用一个不会报错的例子来说明. 请注意以下代码即使在 strict: true
的条件下也可以通过编译, 但是运行时会出现类型错误.
const cat = new Cat('Tom');
const cats: Cat[] = [cat];
function handle(animals: Animal[]) {
cats.push(new Animal('animal'));
}
handle(cats);
cats.forEach(cat => {
cat.catchJerry(); // oops!
});
上述代码调用 handle
时传入了 Cat[]
, 但是 handle
期望接收的参数是 Animal[]
, 于是直接往里面插入了一个 Animal
. 此时 Cat[]
中混进了一个 Animal
, 但是 Cat[]
并不知道, 依旧将所有元素当做 Cat
使用, 自然会出问题.
但是反过来, 假设 handle
期望接收的参数是 Cat[]
, 显而易见的, 我们也不能传 Animal[]
进去. 我们只能传一个类型完全为 T[]
的参数进去. 这种场景就叫做 invariance, 只能放完全相同的类型, 不能放父类型或者子类型.
再次强调, 这段示例代码只是为了表达 T[]
对于 T
是 invariant, 但是实际上 TS 并没有这么处理, TS 的处理是 T[]
对于 T
是 covariant.
数据流向
接下来我们仔细分析一下两者的差异, 为什么同样在需要某个类型的位置, 有时候可以传入子类型, 有时候不可以? 原因在于读写的区别.对于只读的场景, 可以传入子类型. 对于只写的场景, 可以传入父类型. 对于读写的场景, 只能传入原类型.
由于 TypeScript 无法按引用传递, 接下来我们使用一段伪代码来说明:
let animal: Animal = some_animal;
let cat:Cat = some_cat;
// readonly
let need_an_animal: Animal = some_cat; // place Child at Parent
need_an_animal.hello();
// writeonly
let need_a_cat: &Cat = &some_animal; // place Parent at Child
*need_a_cat = another_cat; // now `some_animal` is pointing to a cat, it's ok
some_animal.hello();
从上述伪代码可以看出, variance 的根本原因是读和写两种操作的数据流向是相反的. 我们称需要的值(参数)为 need
, 实际传入的值为 real
, 在读操作下, 数据是从 real
流向 need
, 所以需要 real
包含所有 need
包含的信息, 即 real
需要是 need
的子类型. 相反, 在写操作下, 数据是从 need
流向 real
, 所以需要 need
包含所有 real
包含的信息, 即 need
需要是 real
的子类型.
可变性和写操作
TS 之所以允许上述 invariance 示例代码通过编译, 是因为 TS 没有声明可变性的机制. 也就是说, TS 编译器不知道 handle
拿到 cats
后会做读操作, 还是写操作. 如果 TS 严格检查, 默认读写操作都会进行, 那么传入的参数就只能是 Animal[]
, 从而失去了很大的灵活性. 所以 TS 在严格度和灵活性之间做了权衡, 决定通过编译.
相反, Rust 强制要求显式声明可变性, 所以上面的代码如果在 Rust 中就会因为 invariance 而报错, 这也是很多生命周期报错的原因.
所以简单总结一下:
- TS 中,
T
类型对于T
类型是 covariant, 因为 TS 认为读操作相比写操作, 更为基础, 所以会放开一些, 优先保护读操作不出错. - Rust 中,
&T
类型对于T
类型是 covariant, 因为不可能通过&T
修改T
, Rust 会保证只读. - Rust 中,
&mut T
类型对于T
类型是 invariant, 因为可以通过&mut T
修改T
, 所以只允许使用完全相同的T
类型.
接下来我们正式开始说明 variance 和生命周期之间的关系.
生命周期中的 variance
Rust 中因为不存在继承, 所以各种普通的类型之间是没有父子关系的. 但是神奇的点是, Rust 的生命周期是有父子关系的. 如果一个生命周期 'a
完全包含了生命周期 'b
, 那么 'a
就是 'b
的子类型. 所以生命周期和 variance 天然地被绑定在了一起.
通过上面的总结我们已经知道了:
&T
类型对于T
类型是 covariance&mut T
类型对于T
类型是 invariance
Rust 官方文档列出了部分 variance
Type | Variance in 'a | Variance in T |
---|---|---|
&'a T | covariant | covariant |
&'a mut T | covariant | invariant |
*const T | covariant | |
*mut T | invariant | |
[T] and [T; n] | covariant | |
fn() -> T | covariant | |
fn(T) -> () | contravariant | |
std::cell::UnsafeCell<T> | invariant | |
std::marker::PhantomData<T> | covariant | |
dyn Trait<T> + 'a | covariant | invariant |
我们重点关注前两行. 请特别注意, &'a T
本身是一个类型, 但是这个类型里包含了另外两个类型: &'a
和 T
. &'a T
在 &'a
和 T
的 variance 是不同的. 请看表格, &'a T
和 &'a mut T
对于 'a
都是 covariant 的. 也就是说在需要 &'a T
/ &'a mut T
的地方, 我们可以放一个 &'b T
/ &'b mut T
, 只要 'b
是 'a
的子类型, 即 'b: 'a
. 请牢牢记住这句话. 这句话将抹杀掉"两个生命周期中的最小值"这个容易产生歧义的理解方式, 让每个引用自己的生命周期更加清晰.
生命周期 covariance
下面这段代码已经被使用过无数次了:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = "hello".to_string(); // 1
let string1_ref = &string1; // 2
let res: &str; // 3
{ // 4
let string2 = "myworld".to_string(); // 5
res = longest(string1_ref, &string2); // 6
} // 7
println!("{}", res); // 8
println!("{}", string1_ref); // 9
}
接下来我们抛开你所熟知的'a 指的是 x 和 y 生命周期中较短的那一个
, 从 variance 角度重新理解. 在 variance 角度下, 'a
将会是一个具体的类型, 而不是"较短的一个", 或者"至少"之类的概念. 我认为一个具体的类型相比"较短的一个","至少", "最多", 会更加容易理解.
为了方便起见, 我会用 1~2
来表示代码中 1 和 2 这两个位置中间的这块生命周期.
上述代码很明显会报错, 因为在 8 处使用的返回值有可能引用的 string2
, 然而 string2
在 7 处就被销毁了. 从泛型的角度来看, longest
函数具有一个叫做 'a
的生命周期泛型参数, 它期望接收生命周期为 'a
的 x
, 和生命周期为 'a
的 y
, 并且返回一个生命周期为 'a
的 &str
类型的值. 于是 Rust 开始进行泛型推断, 基于调用时的上下文:
string1_ref
的生命周期为2~9
, 并且最多可以被延长到2~10
&string2
的生命周期为6~6
, 并且最多可以被延长到6~8
res
期望的生命周期为6~8
(不是从 3 开始, 因为 3 处的res
没有任何读取操作, 8 处的对res
读取的是在6
处赋的值)
Rust 对于上述错误代码的推理过程为:
- 将
6~8
代入'a
, 因为返回值要求至少拥有6~8
的生命周期longest
的函数签名就变成了: 期望接收两个生命周期为6~8
的参数, 返回一个生命周期为6~8
的值. - 接下来检查第一个参数. 第一个参数期望接收的生命周期为
6~8
, 而传入的实际参数string1_ref
生命周期为2~9
,2~9
是6~8
的子类型, 符合 covariance, 通过. - 接下来检查第二个参数. 第二个参数期望接收的生命周期为
6~8
, 而传入的实际参数&string2
生命周期为6~6
. Rust 试图延长&string2
的生命周期, 但是最多延长到6~7
.6~7
不是6~8
的子类型(而是父类型), 编译失败并且报错:
error[E0597]: `string2` does not live long enough
--> src/bin/lifetime.rs:17:36
|
16 | let string2 = "myworld".to_string(); // 5
| ------- binding `string2` declared here
17 | res = longest(string1_ref, &string2); // 6
| ^^^^^^^^ borrowed value does not live long enough
18 | } // 7
| - `string2` dropped here while still borrowed
19 |
20 | println!("{}", res); // 8
| --- borrow later used here
这样报错信息我们就完全可以理解了. &string2
预期生命周期至少为 6~8
, 但是实际生命周期最多被延长为 6~7
, 所以 &string2 does not live long enough
, 活得不够长.
上面我们理解了生命周期报错背后的 variance 原理, 接下来用同样的推理过程看看正确的代码是如何经过 covariance 而通过编译的:
fn main() {
let string1 = "hello".to_string(); // 1
let string1_ref = &string1; // 2
let res: &str; // 3
{ // 4
let string2 = "myworld".to_string(); // 5
res = longest(string1_ref, &string2); // 6
println!("{}", res); // 7
} // 8
println!("{}", string1_ref); // 9
} // 10
'a
先被推断为6~7
.- 第一个参数
string1_ref
的生命周期本来是2~9
, 预期是6~7
, 通过 covariance,2~9
可以赋值给6~7
. - 第二个参数
&string2
的生命周期本来是6~6
, 但是因为预期是6~7
, Rust 试图延长其生命周期到6~7
, 成功了.
请注意, 此时 &string2
的生命周期被延长为了 6~7
. 我们可以简单证明一下. Rust 不允许对同一个值的可变引用和不可变引用有重叠的生命周期部分, 我们在 7 上方加一行:
let mut string2 = "myworld".to_string(); // 5, make it mut
res = longest(string1_ref, &string2); // 6
println!("{}", &mut string2); // add a mutable reference
println!("{}", res); // 7
此时编译器报错了:
17 | res = longest(string1_ref, &string2); // 6
| -------- immutable borrow occurs here
18 | println!("{}",&mut string2);
| ^^^^^^^^^^^^ mutable borrow occurs here
19 | println!("{}", res); // 7
| --- immutable borrow later used here
可以看出, &string2
的生命周期确实被延长到了 7 处, 所以我们才不能在 6 和 7 之间使用一个 &mut string2
.
生命周期 invariance
回顾上面的 variance table, 我们知道 &'a T
对于 T
是 covariant, 但是 &'a mut T
对于 T
是 invariant. 用一个简单的例子证明一下:
fn test<'a>(vec: &'a Vec<&'a i32>) {
todo!()
}
static GLOBAL_1: i32 = 1;
static GLOBAL_2: i32 = 2;
static GLOBAL_3: i32 = 3;
fn main() {
let vec: Vec<&'static i32> = vec![&GLOBAL_1, &GLOBAL_2, &GLOBAL_3]; // 1
test(&vec); // 2
println!("{:?}", vec); // 3
} // 4
test
函数希望接收一个参数, 参数是一个 vec
引用, 生命周期为 'a
. vec 内部的元素是对 i32
的引用, 生命周期也是 'a
. 在 main
中, 我们手动创建了一个 Vec<&'static i32>
的 vec
, 然后将 &vec
传给了 test
. 此时的上下文为:
formal param: &'a Vec<&'a i32>
actual param: &2~2 Vec<&'static i32>
泛型推理总是倾向于父类型, 所以 'a
会被推断为 2~2
, 而不是 'static
. 然后, 内部'static
通过 covariance 赋值给 2~2
. 编译成功.
接下来看看 mut
的情况:
fn test<'a>(vec: &'a mut Vec<&'a i32>) {
todo!()
}
static GLOBAL_1: i32 = 1;
static GLOBAL_2: i32 = 2;
static GLOBAL_3: i32 = 3;
fn main() {
let mut vec: Vec<&'static i32> = vec![&GLOBAL_1, &GLOBAL_2, &GLOBAL_3]; // 1
test(&mut vec); // 2
println!("{:?}", vec); // 3
} // 4
类似的, 我们梳理一下推理上下文:
formal param: &'a mut Vec<&'a i32>
actual param: &2~2 mut Vec<&'static i32>
&'a mut T
对于 T
是 invariant, 而此时 T
是 Vec<&'static i32>
, 所以 'a
只能被强制推断为 'static
, test
就变成了 fn test<'static>(vec: &'static mut Vec<&'static i32>)
, 然而 &mut vec
的生命周期并不是 'static
, 所以编译失败报错:
error[E0597]: `vec` does not live long enough
--> src/bin/lifetime.rs:11:10
|
10 | let mut vec: Vec<&'static i32> = vec![&GLOBAL_1, &GLOBAL_2, &GLOBAL_3]; // 1
| ------- ----------------- type annotation requires that `vec` is borrowed for `'static`
| |
| binding `vec` declared here
11 | test(&mut vec); // 2
| ^^^^^^^^ borrowed value does not live long enough
12 | println!("{:?}", vec);
13 | }
| - `vec` dropped here while still borrowed
error[E0502]: cannot borrow `vec` as immutable because it is also borrowed as mutable
--> src/bin/lifetime.rs:12:22
|
10 | let mut vec: Vec<&'static i32> = vec![&GLOBAL_1, &GLOBAL_2, &GLOBAL_3]; // 1
| ----------------- type annotation requires that `vec` is borrowed for `'static`
11 | test(&mut vec); // 2
| -------- mutable borrow occurs here
12 | println!("{:?}", vec);
| ^^^ immutable borrow occurs here
Rust 的确将 'a
推理成了 'static
. 从报错信息可以看出来:
type annotation requires that `vec` is borrowed for `'static`
这个推理结果就会导致这两个错误:
vec
活得不如'static
久, 毕竟它在 4 处就被 drop 了.&mut vec
被标记为了'static
, 它"可以"活到永久, 所以任何其他对vec
的引用都被禁止.
更为复杂的例子
接下来我们看一个更复杂的经典例子:
struct Interface<'a> {
manager: &'a mut Manager<'a>,
}
impl<'a> Interface<'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str,
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
pub fn get_interface(&'a mut self) -> Interface<'a> {
Interface {
manager: &mut self.manager,
}
}
}
fn main() {
let mut list = List { // 1
manager: Manager { text: "hello" },
};
list.get_interface().noop(); // 2
println!("Interface should be dropped here and the borrow released"); // 3
// this fails because inmutable/mutable borrow
// but Interface should be already dropped here and the borrow released
use_list(&list); // 4
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
上述代码在 4 处会报错:
error[E0502]: cannot borrow `list` as immutable because it is also borrowed as mutable
--> src/bin/lifetime.rs:39:14
|
33 | list.get_interface().noop(); // 2
| ---- mutable borrow occurs here
...
39 | use_list(&list); // 4
| ^^^^^
| |
| immutable borrow occurs here
| mutable borrow later used here
我们从 variance 的角度去找一下原因和修复方式.
- 2 处,
list.get_interface()
会自动创建一个&'b mut list<'a>
, 因为list.get_interface()
实际上是List::get_interface(&mut list)
的语法糖. list
包含生命周期'a
. 请注意是包含, 而不是list
自己的生命周期.'a
通过观察可以看出, 是1~4
, 因为在 1 和 4 处都有使用到, 所以'a = 1~4
.- 看一下
get_interface
的函数签名, 对于包含生命周期'a
的list
, 参数为&'a mut self
, 即&'a mut list<'a>
此时的推理上下文:
formal param: &'a mut list<'a>
actual param: &'b mut list<1~4>
由于 &'a mut list<'a>
对于 list<'a>
是 invariant, 'a
会被直接代入为 1~4
, 并且 'b
也会被直接代入为 1~4
(实际上由于 covariance, b
可以比 1~4
更大). 也就是说, 在 3 处, 我们隐式地创建了一个 &1~4 mut list<1~4>
匿名可变引用. 这个匿名可变引用的生命周期会一直持续到 4, 所以 4 处会报错: 同时使用了可变引用和不可变引用.
我们尝试一下修复.
修复方式 1
虽然 &'a mut T
对于 T
是 invariant, 但是 &'a T
对于 T
是 covariant. 所以我们只需要去掉所有的 mut, 就可以将其转变为 covariant, 避免第一个 'a
被连带着强制代入 T
中的 'a
.
struct Interface<'a> {
manager: &'a Manager<'a>,
}
impl<'a> Interface<'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str,
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
pub fn get_interface(&'a self) -> Interface<'a> {
Interface {
manager: &self.manager,
}
}
}
fn main() {
let mut list = List { // 1
manager: Manager { text: "hello" },
};
list.get_interface().noop(); // 2
println!("Interface should be dropped here and the borrow released"); // 3
// this fails because inmutable/mutable borrow
// but Interface should be already dropped here and the borrow released
use_list(&mut list); // 4
}
fn use_list(list: &mut List) {
println!("{}", list.manager.text);
}
此时 'a
会被推断为 2~2
, 而 'b
为 1~4
, 经过 covariance 放在了 'a
的位置, 编译通过. 并且我们将 4 处改为 &mut
也没有报错, 说明此时 2 处创建的隐式 &list
生命周期确实没有到 4 处.
修复方式 2
上述方式去掉了 mut
, 失去了 Interface
的可变性. 接下来我们看看保留 mut
的方式.
既然 &'a mut List<'a>
对于 List<'a>
是 invariant, 而 List<'a>
会被强制推断成 List<1~4>
, 那我们就将第一个 'a
换成另一个生命周期泛型 'b
, 将两者解除绑定. 预期结果是, 'a
被推断成 1~4
, 'b
被推断成 2~2
, 这样创建的临时匿名可变引用的生命周期就是 2~2
, 不会影响到后续 4 处创建的不可变引用.
pub fn get_interface<'b>(&'b mut self) -> Interface<'a>
此时编译器报错了:
|
19 | impl<'a> List<'a> {
| -- lifetime `'a` defined here
20 | pub fn get_interface<'b>(&'b mut self) -> Interface<'a> {
| -- lifetime `'b` defined here
21 | / Interface {
22 | | manager: &mut self.manager,
23 | | }
| |_________^ method was supposed to return data with lifetime `'a` but it is returning data with lifetime `'b`
此时的生命周期大致长这样(伪代码):
pub fn get_interface<'b>(self: &'b mut List<'a>) -> Interface<'a> {
Interface {
manager: &'b mut self.manager<'a>,
}
}
请特别注意, self.manager
的生命周期和 self.manager
包含的生命周期是不一样的. 我们在函数内部创建并返回的 &mut self.manager<'a>
, 这个可变引用的生命周期是 'b
, 而不是 'a
, 因为它来自参数 &'b mut List<'a>
, 这个参数是一个带着 'b
生命周期的可变引用.
编译器告诉我们需要添加 'b: 'a
. 但是我们不能这么做. 一旦这么做了, 'b
的生命周期就会至少变成 1~4
, 又陷入之前同样的问题.
根本原因在于返回的 Interface
中携带的 manager
引用的生命周期不应该为 'a
, 而是应该为 'b
, 所以我们需要更新 Interface<'a>
, 将其自身引用的生命周期和引用目标的引用的生命周期分开:
struct Interface<'b, 'a> {
manager: &'b mut Manager<'a>,
}
impl<'b, 'a> Interface<'b, 'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
pub fn get_interface<'b>(&'b mut self) -> Interface<'b, 'a>
到此, 编译成功. 'b
会被推断为 2~2
, 'a
会被推断为 1~4
.
contravariance
最后简单介绍一下 contravariance, 这个主要用于函数类型.
在 TypeScript 中, T
对于 T
是 covariant, 但是 (param: T) => void
对于 T
是 contravariant.
function handleFn(callback: (items: Cat[]) => void) {
callback([new Cat('Tom')]);
}
handleFn((items: Animal[]) => {
items.push(new Animal('Tom'));
});
handleFn
的参数是一个函数: (items: Cat[]) => void
, 但是实际上可以传入 (items: Animal[]) => void
, 因为 handleFn
在调用这个函数的时候, 会传入 items
, 数据流向是从 handleFn
流向 callback
, callback
通过读取形参 items
, 从而读取实参 items
. callback
只能将其当做更 base 的类型去读, 才能保证不读取错误.
反过来则不行. 假设 callback
期望的类型是 (items: Animal[]) => void
, 实际传入的 callback
是 (items: Cat[]) => void
, 那么会导致, handleFn
传入的是 Animal[]
, 但是 callback
将其当做 Cat[]
来读, 发生错误.
总结
本文结合 TypeScript 和 Rust 介绍了 variance, 从 variance 的角度理解了生命周期.
&'a T
对于'a
和T
都是 covariant, 意味着可以用'a
的子类型替代'a
, 用T
的子类型替代T
&'a mut T
对于'a
是 covariant, 但是对于T
是 invariant, 所以如果T
中带有生命周期, 那么该生命周期将被强制代入对应的生命周期泛型参数.
小技巧
一般情况下, 永远不要写出
impl<'a> SomeStruct<'a> {
fn some_method(&'a mut self) {}
}
这样的代码, 因为一旦调用 some_struct.some_method()
, 自动创建的匿名可变引用的生命周期(&'a mut self
中的 'a
)将和 self
的生命周期 (SomeStruct<'a>
中的 'a
) 绑定, 从而导致后续再也无法使用 some_struct
. 因为一旦使用, some_struct
的生命周期就会延续到使用的位置, 导致匿名可变引用的生命周期同步延续到使用的位置, 产生同时存在可变引用和不可变引用的冲突问题.
转载自:https://juejin.cn/post/7426947654775357475