bigywater.tech
Rust · 18

Borrow Checker 如何阻止悬垂引用

悬垂引用(指向已释放内存的引用)是 C/C++ 里最常见的崩溃来源,Rust 在编译期就把它们拒之门外。这篇把三种典型的悬垂场景各编译一遍、读编译器的报错,再用 unsafe 偷看一眼 Vec 重新分配时元素真的搬家了 —— 这就是编译器拒绝它们的原因。

E0597
`x` does not live long enough —— 引用比值活得久
E0502
持有元素引用时 push 的错误
cap 3 → 6
push 触发重新分配,v[0] 的地址改变

返回局部变量的引用

实测 · 本机执行输出
  源码:
      1 | fn dangle() -> &String {
      2 |     let s = String::from("hello");
      3 |     &s
      4 | }
      5 | fn main() { println!("{}", dangle()); }
  编译:失败
    error[E0106]: missing lifetime specifier
     --> <实验目录>/src/u18a.rs:1:16
      |
    1 | fn dangle() -> &String {
      |                ^ expected named lifetime parameter
      |
      = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
    help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
      |
    1 | fn dangle() -> &'static String {
      |                 +++++++
    help: instead, you are more likely to want to return an owned value
      |
    1 - fn dangle() -> &String {
    1 + fn dangle() -> String {
      |
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0106`.
  源码:
      1 | fn dangle<'a>() -> &'a String {
      2 |     let s = String::from("hello");
      3 |     &s
      4 | }
      5 | fn main() { println!("{}", dangle()); }
  编译:失败
    error[E0515]: cannot return reference to local variable `s`
     --> <实验目录>/src/u18b.rs:3:5
      |
    3 |     &s
      |     ^^ returns a reference to data owned by the current function
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0515`.

引用比值活得久

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let r;
      3 |     {
      4 |         let x = 5;
      5 |         r = &x;
      6 |     }
      7 |     println!("r = {}", r);
      8 | }
  编译:失败
    error[E0597]: `x` does not live long enough
     --> <实验目录>/src/u18c.rs:5:13
      |
    4 |         let x = 5;
      |             - binding `x` declared here
    5 |         r = &x;
      |             ^^ borrowed value does not live long enough
    6 |     }
      |     - `x` dropped here while still borrowed
    7 |     println!("r = {}", r);
      |                        - borrow later used here
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0597`.

Vec 的悬垂

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let mut v = vec![1, 2, 3];
      3 |     let first = &v[0];
      4 |     v.push(4);
      5 |     println!("first = {}", first);
      6 | }
  编译:失败
    error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
     --> <实验目录>/src/u18d.rs:4:5
      |
    3 |     let first = &v[0];
      |                  - immutable borrow occurs here
    4 |     v.push(4);
      |     ^^^^^^^^^ mutable borrow occurs here
    5 |     println!("first = {}", first);
      |                            ----- immutable borrow later used here
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0502`.
  为什么要禁止?用 unsafe 的裸指针亲眼看看 push 之后元素搬家了(只是为了观察,真实代码不要这么写):
  编译:成功
  运行(退出码 0):
    push 之前 cap = 3,v[0] 的地址 = 0x100b65300
    push 之后 cap = 6,v[0] 的地址 = 0x100b65170
    地址变了:true(旧地址上的内存已经被释放)

借用规则的细节见不可变借用与可变借用、可变借用为什么不能同时存在;容量增长见Vec 扩容机制。

合法的写法

实测 · 本机执行输出
  源码:
      1 | fn longest_owned() -> String { let s = String::from("hello"); s }       // 返回所有权
      2 | fn main() {
      3 |     println!("{}", longest_owned());
      4 |     let r; let x = 5; r = &x;                                           // x 活得比 r 久
      5 |     println!("r = {r}");
      6 |     let mut v = vec![1, 2, 3];
      7 |     let first = v[0];                                                   // 拷贝出值,不再持有引用
      8 |     v.push(4);
      9 |     println!("first = {first},v = {:?}", v);
     10 | }
  编译:成功
  运行(退出码 0):
    hello
    r = 5
    first = 1,v = [1, 2, 3, 4]

分步演示

悬垂引用
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。地址对比用 unsafe 裸指针只做观察;扩容的容量值(3→6)是当前标准库的行为
    • 没有验证:Polonius 之类更精确的借用检查、self-referential 结构体、Box 的悬垂检查、编译器是怎么在 MIR 上计算 borrow 区域的(见 NLL 一篇)
    京ICP备2026067502号-1京公网安备11010502063487号