bigywater.tech
Rust · 22

NLL 非词法生命周期

NLL 是 Rust 2018 之后借用检查的基础:借用不再「到块结束」,而是「到最后一次使用」。这篇用几个对比实验验证它做了什么、没做什么:控制流里的借用、两阶段借用、Drop 为什么会让借用变长,最后直接看 rustc 内部保存的区域信息。

最后一次使用
NLL 下借用的结束位置
两阶段借用
v.push(v.len()) 合法的原因
MIR 点集
NLL 区域在 rustc 内部的表示(-Z dump-mir)

借用到最后一次使用

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let mut v = vec![1, 2, 3];
      3 |     let first = &v[0];
      4 |     println!("first = {first}");      // first 的最后一次使用
      5 |     v.push(4);                         // 旧的「按作用域」规则会拒绝这一行;NLL 允许
      6 |     println!("{:?}", v);
      7 |     let r = &mut v;
      8 |     r.push(5);
      9 |     v.push(6);                         // r 已经不再使用
     10 |     println!("{:?}", v);
     11 | }
  编译:成功
  运行(退出码 0):
    first = 1
    [1, 2, 3, 4]
    [1, 2, 3, 4, 5, 6]

控制流

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let mut s = String::from("a");
      3 |     let r = &s;
      4 |     let flag = std::env::args().count() > 5;
      5 |     if flag { println!("{r}"); }       // 只有这条路径用 r
      6 |     else { s.push('b'); }             // 另一条路径里 r 已经死了
      7 |     println!("{s}");
      8 | }
  编译:成功
  运行(退出码 0):
    ab

两阶段借用

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let mut v = vec![1, 2, 3];
      3 |     v.push(v.len());
      4 |     println!("{:?}", v);
      5 | }
  编译:成功
  运行(退出码 0):
    [1, 2, 3, 3]
  源码:
      1 | fn main() {
      2 |     let mut v = vec![1, 2, 3];
      3 |     let r = &mut v;
      4 |     r.push(v.len());     // 先取走了 &mut,再读 v:冲突
      5 | }
  编译:失败
    error[E0502]: cannot borrow `v` as immutable because it is also borrowed as mutable
     --> <实验目录>/src/u22d.rs:4:12
      |
    3 |     let r = &mut v;
      |             ------ mutable borrow occurs here
    4 |     r.push(v.len());     // 先取走了 &mut,再读 v:冲突
      |       ---- ^ immutable borrow occurs here
      |       |
      |       mutable borrow later used by call
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0502`.

Drop 与借用

实测 · 本机执行输出
  源码:
      1 | struct Guard<'a>(&'a mut Vec<i32>);
      2 | impl Drop for Guard<'_> { fn drop(&mut self) { println!("Guard 释放,vec 长度 {}", self.0.len()); } }
      3 | fn main() {
      4 |     let mut v = vec![1];
      5 |     let g = Guard(&mut v);
      6 |     v.push(2);
      7 | }
  编译:失败
    error[E0499]: cannot borrow `v` as mutable more than once at a time
     --> <实验目录>/src/u22e.rs:6:5
      |
    5 |     let g = Guard(&mut v);
      |                   ------ first mutable borrow occurs here
    6 |     v.push(2);
      |     ^ second mutable borrow occurs here
    7 | }
      | - first borrow might be used here, when `g` is dropped and runs the `Drop` code for type `Guard`
    
    warning: unused variable: `g`
     --> <实验目录>/src/u22e.rs:5:9
      |
    5 |     let g = Guard(&mut v);
      |         ^ help: if this is intentional, prefix it with an underscore: `_g`
      |
      = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default
    
    error: aborting due to 1 previous error; 1 warning emitted
    
    For more information about this error, try `rustc --explain E0499`.
  源码:
      1 | struct Guard<'a>(&'a mut Vec<i32>);
      2 | impl Drop for Guard<'_> { fn drop(&mut self) { println!("Guard 释放,vec 长度 {}", self.0.len()); } }
      3 | fn main() {
      4 |     let mut v = vec![1];
      5 |     { let _g = Guard(&mut v); }      // 缩小 Guard 的作用域
      6 |     v.push(2);
      7 |     println!("{:?}", v);
      8 | }
  编译:成功
  运行(退出码 0):
    Guard 释放,vec 长度 1
    [1, 2]

Drop 的规则见Drop Trait 与资源释放。

NLL 的区域

实测 · 本机执行输出
  生成的 NLL 相关 MIR 转储文件: ['u22g.main.-------.nll.0.mir', 'u22g.main.-------.nll.0.regioncx.all.dot', 'u22g.main.-------.nll.0.regioncx.scc.dot'] (共 3 个)
  NLL 区域信息(借用区域 '?N 覆盖的 MIR 位置)节选:
    | '?0 | Global | ['?0, '?1]
    | '?1 | Local | ['?1]
    | '?0 | U0 | {bb0[0..=3], bb1[0..=2], bb2[0..=1], bb3[0..=6], bb4[0..=16], bb5[0..=12], bb6[0..=4], bb7[0..=9], bb8[0..=5], bb9[0..=1], bb10[0], bb11[
    | '?1 | U0 | {bb0[0..=3], bb1[0..=2], bb2[0..=1], bb3[0..=6], bb4[0..=16], bb5[0..=12], bb6[0..=4], bb7[0..=9], bb8[0..=5], bb9[0..=1], bb10[0], bb11[
    | '?2 | U0 | {bb3[5..=6], bb4[0..=16], bb5[0..=12], bb6[0..=4]}
    | '?3 | U0 | {bb4[1..=16], bb5[0..=12], bb6[0..=4]}
    | '?4 | U0 | {bb4[8..=16], bb5[0..=12], bb6[0..=4]}
    | '?5 | U0 | {bb4[15..=16], bb5[0..=12], bb6[0..=4]}

MIR 是什么见MIR 是什么。-Z 选项靠 RUSTC_BOOTSTRAP=1 在稳定版上打开,仅用于观察。

分步演示

NLL
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。MIR 转储来自 rustc 1.98.1 的 -Z dump-mir=main,格式随版本变化
    • 没有验证:Polonius(更精确的下一代借用检查,未稳定)、"问题案例 #3"(条件返回借用)在当前版本是否被接受、闭包与 NLL 的交互、对 async 状态机里借用的处理
    京ICP备2026067502号-1京公网安备11010502063487号