cisen/blog

rust 2049 NLL

Opened this issue · 1 comments

cisen commented

总结

// 一个约束集合 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 = &fooB/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
    • 约束存活是在定义的下一行,在其调用的同一行
    • 子类约束一般用在定义新的变量
  • 重借用约束:
cisen commented

问题示例 #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 开始)。这正是我们想要的。