rust 2049 NLL
Opened this issue · 1 comments
总结
- 译文:https://zhuanlan.zhihu.com/p/434820800
- 生命周期不是类型,赋值的逻辑是不同的
// 一个约束集合 C:
C = true
| C, (L1: L2) @ P // 点 P 之后生命周期 L1 大于等于生命周期 L2
// 一个生命周期 L:
L = 'a
| {P}
let mut foo: T = ...;
let mut bar: T = ...;
let mut p: &'p T = &foo;
// `p` 在这里是存活的,因为它的值之后可能会被用到
if condition {
// `p` 在这里是存活的,因为它的值之后可能会被用到
print(*p);
// `p` 在这里不是存活的,因为它现在所持有的值不会再被用到
p = &bar;
// `p` 在这里是存活的,因为它的值(刚刚赋予的)之后可能会被用到
}
// `p` 在这里是存活的,因为它的值之后可能会被用到
print(*p);
// `p` 在这里不是存活的,因为它的值不会再被用
let mut foo: T = ...;
let mut bar: T = ...;
let mut p: &T;
p = &foo;
// (0)
if condition {
print(*p);
// (1)
p = &bar;
// (2)
}
// (3)
print(*p);
// (4)
这个示例的关键在于,foo 只在 0 和 3 处被借用,在 1 处没被借用。bar 则是在 2 和 3 处被借用。foo 和 bar 在 4 处都没有被借用,因为在 4 处 p 并没有被使用。
我们可以把个示例转换成下面的控制流图。之前说过,MIR 中的控制流图由基础块组成,而基础块则由一些独立的语句后跟一个终结指令构成:
// let mut foo: i32;
// let mut bar: i32;
// let mut p: &i32;
A
[ p = &foo ]
[ if condition ] ----\ (true)
| |
| B v
| [ print(*p) ]
| [ ... ]
| [ p = &bar ]
| [ ... ]
| [ goto C ]
| |
+-------------/
|
C v
[ print(*p) ]
[ return ]
接下来我们用 基础块/索引 形式来指向控制流图中的某个语句或者终结指令。举个例子,A/0
指向的是 p = &foo
,B/4
指向的是 goto C
。
约束
像 ('a: 'b) @ P
这样一个约束的意义是,从点 P 开始,生命周期 'a
必须包含 'b
中从点 P 开始能够到达的所有点。这个实现从点 P 开始深度优先搜索,超出 'b
则停止搜索,过程中遇到的所有 'b
中的点(注意,在 B/2 处 p 被重新赋值,因此那之后一直到 if 结束都是从 A/1 开始所无法到达的),我们都把它加到 'a
中。
在上面的 示例 4 中,全部约束如下:
('foo: 'p) @ A/1
('bar: 'p) @ B/3
('p: {A/1}) @ A/1
('p: {B/0}) @ B/0
('p: {B/3}) @ B/3
('p: {B/4}) @ B/4
('p: {C/0}) @ C/0
为了满足这些约束,我们得到了如下的生命周期,就跟我们之前预期的结果一样:
'p = {A/1, B/0, B/3, B/4, C/0}
'foo = {A/1, B/0, C/0}
'bar = {B/3, B/4, C/0}
问答
('a: 'b) @ P
, 'a大还是‘b大’
('父: '子) @P
'b
大,详见:(&'foo T <: &'p T) @ A/1
的解析- 这句话的意思是'a是'b的子生命周期,'a在@p点开始生效,p点是复制语句之后的下一条语句。
如果'a在一个大括号里面,'b出去大括号后,'a还没结束?
为什么会有(&'foo T <: &'p T) @ A/1
和('p: {A/1}) @ A/1
的混合?
('p: {A/1}) @ A/1
是基于存活的约束
约束一共有多少个类型?
- 3个,面向调用语句的生命周期存活约束,面向赋值语句的值类型约束,面向解引用的重借用约束
- 存活约束:
('p: {A/1}) @ A/1
-
- 约束存活是在定义的下一行,在其调用的同一行
-
- 存活约束一般用在调用
- 子类约束:
('a: 'b) @ P
, -
- 约束存活是在定义的下一行,在其调用的同一行
-
- 子类约束一般用在定义新的变量
- 重借用约束:
问题示例 #2.
fn process_or_default() {
let mut map = ...;
let key = ...;
match map.get_mut(&key) { // -------------+ 'lifetime
Some(value) => process(value), // |
None => { // |
map.insert(key, V::default()); // |
// ^~~~~~ ERROR. // |
} // |
} // <------------------------------------+
}
编译成 MIR,就像下面这样(一些无关的细节被省略了)。注意 match 语句被编译成一个 SWITCH 和 一个 downcast,SWITCH 测试 tmp2 看看是走哪个分支,downcast 把 Some 包含的内容提取出来(这个操作是 MIR 独有的,作为 match 的一部分生成的)。
let map: HashMap<K,V>;
let key: K;
let tmp0: &'tmp0 mut HashMap<K,V>;
let tmp1: &K;
let tmp2: Option<&'tmp2 mut V>;
let value: &'value mut V;
START {
/*0*/ map = ...;
/*1*/ key = ...;
/*2*/ tmp0 = &'map mut map;
/*3*/ tmp1 = &key;
/*4*/ tmp2 = HashMap::get_mut(tmp0, tmp1);
/*5*/ SWITCH tmp2 { None => NONE, Some => SOME }
}
NONE {
/*0*/ ...
/*1*/ goto EXIT;
}
SOME {
/*0*/ value = tmp2.downcast<Some>.0;
/*1*/ process(value);
/*2*/ goto EXIT;
}
EXIT {
}
基于存活的约束:
- 约束存活是在定义的下一行,在其调用的同一行
- 存活的约束不能合并
('tmp0: {START/3}) @ START/3
('tmp0: {START/4}) @ START/4
('tmp2: {SOME/0}) @ SOME/0
('value: {SOME/1}) @ SOME/1
子类型约束:
- 子类约束存活是在定义的下一行,在其调用的同一行
- 子类的约束会被合并
('map: 'tmp0) @ START/3
# 合并生命周期的时候temp的START/5会被隐藏
('tmp0: 'tmp2) @ START/5
('tmp2: 'value) @ SOME/1
最后,我们最感兴趣的生命周期就是 'map,也就是 map 被借用的范围。满足上面的约束我们得到:
'map == {START/3, START/4, SOME/0, SOME/1}
'tmp0 == {START/3, START/4, SOME/0, SOME/1}
'tmp2 == {SOME/0, SOME/1}
'value == {SOME/1}
结果说明 map 在 None 分支可以被修改;map 在 Some 分支也可以被修改,但必须在 process() 之后(即:从 SOME/2 开始)。这正是我们想要的。