Rust 编译器与运行时揭秘

第2章 所有权系统:编译期内存管理的核心机制

作者 杨艺韬 · 10,967 字

第2章 所有权系统:编译期内存管理的核心机制

"Ownership is Rust's most unique feature, and it enables Rust to make memory safety guarantees without needing a garbage collector." —— The Rust Programming Language

本章要点

  • 所有权模型的核心:每个值有且仅有一个所有者,所有权可以转移(move),所有者离开作用域时值被销毁
  • Move 在 MIR 中表现为 Operand::Move(place),将源 place 标记为未初始化,后续访问触发编译错误
  • Copy 与 Move 的区别在于 MIR 中使用 Operand::Copy 还是 Operand::Move,由 Copy trait 判定
  • Copy 和 Drop 互斥(E0184),同时实现会导致 double free
  • Drop elaboration 是 MIR 的关键 pass:分析所有控制流路径,将条件性 Drop 转换为确定性 Drop,必要时插入 drop flag
  • 部分移动(partial move)使结构体进入"部分初始化"状态,编译器为每个字段独立追踪
  • 析构顺序严格确定:局部变量按声明逆序,结构体字段按声明正序

2.1 所有权模型:每个值有且仅有一个所有者

Rust 的所有权系统建立在三条规则之上:每个值都有一个所有者;同一时刻只能有一个所有者;当所有者离开作用域时值被丢弃。从编译器的视角看,这三条规则的核心目标只有一个:

保证每个拥有析构函数(Drop)的值在所有控制流路径上恰好被销毁一次。

不多不少。不销毁意味着资源泄漏,销毁两次意味着 double free。

fn ownership_basics() {
    let s = String::from("hello");  // s 获得所有权
    let t = s;                       // 所有权转移给 t,s 不再有效
    // println!("{}", s);            // 编译错误:value used here after move
    println!("{}", t);               // OK:t 是所有者
}   // t 在这里被自动析构,释放堆内存

所有权不仅管理内存,也管理所有需要"清理"的资源——文件句柄、网络连接、互斥锁。这就是 Rust 版本的 RAII。与 C++ 不同的是,Rust 编译器强制执行所有权规则,违规是编译期错误而非运行期崩溃。

所有权转移发生在赋值(let t = s)、函数参数传递(foo(s))、函数返回值(return s)、模式匹配(let (a, b) = pair)等场景中。这些在编译器内部被统一表示为 MIR 中的 Operand::Move

2.2 Move 语义在编译器层面的实现

2.2.1 MIR 中的 Move 操作

一个简单的 move 操作在 MIR 中的表示:

fn move_example() {
    let s = String::from("hello");
    let t = s;
    println!("{}", t);
}

编译器将其降低为如下 MIR(简化):

fn move_example() -> () {
    let _1: String;               // s
    let _2: String;               // t

    bb0: {
        StorageLive(_1);
        _1 = String::from("hello");
        StorageLive(_2);
        _2 = move _1;             // Operand::Move —— 关键!
        // _1 从此处起处于未初始化状态
        _0 = std::io::_print(/* 使用 _2 */);
        drop(_2);                  // t 的析构点
        StorageDead(_2);
        StorageDead(_1);           // _1 的值已被移走,不调用 Drop
        return;
    }
}

在编译器源码 compiler/rustc_middle/src/mir/syntax.rs 中,Operand 的定义揭示了 Move 和 Copy 的本质区别:

// 源码:compiler/rustc_middle/src/mir/syntax.rs
pub enum Operand<'tcx> {
    /// 加载 place 的值。drop elaboration 之前,place 的类型必须是 Copy。
    Copy(Place<'tcx>),

    /// 加载 place 的值,并*可能*将 place 覆写为 uninit。
    Move(Place<'tcx>),

    /// 常量值。
    Constant(Box<ConstOperand<'tcx>>),
}

关键信息:Operand::Copy 在 drop elaboration 之前只能用于 Copy 类型;Operand::Move 会将源 place 标记为未初始化。

2.2.2 Move 的物理本质:memcpy

一个常见误解是"move 比 copy 更高效"。实际上,从机器码层面看,两者执行的操作完全相同——都是一次 memcpy。区别仅在编译器的静态分析层面:Copy 后源变量仍有效,Move 后源变量被禁止访问。

graph LR
    subgraph "Move 之前"
        S1["_1 (s)<br/>ptr → heap: 'hello'<br/>len: 5, cap: 5"]
    end
    subgraph "Move 之后"
        S2["_1 (s)<br/>未初始化(禁止访问)"]
        S3["_2 (t)<br/>ptr → heap: 'hello'<br/>len: 5, cap: 5"]
    end
    S1 -->|"memcpy + 标记未初始化"| S3

    style S1 fill:#3b82f6,color:#fff,stroke:none
    style S2 fill:#ef4444,color:#fff,stroke:none
    style S3 fill:#10b981,color:#fff,stroke:none

这就是所有权系统"零开销"的本质——所有权转移完全是编译器跟踪的静态信息,没有引用计数的原子操作,没有 GC 暂停。

2.2.3 MoveData:编译器的移动追踪基础设施

编译器通过 MoveData 结构追踪所有 move 操作,定义在 compiler/rustc_mir_dataflow/src/move_paths/mod.rs 中:

// 源码:compiler/rustc_mir_dataflow/src/move_paths/mod.rs
pub struct MoveData<'tcx> {
    pub move_paths: IndexVec<MovePathIndex, MovePath<'tcx>>,
    pub moves: IndexVec<MoveOutIndex, MoveOut>,
    pub loc_map: LocationMap<SmallVec<[MoveOutIndex; 4]>>,
    pub path_map: IndexVec<MovePathIndex, SmallVec<[MoveOutIndex; 4]>>,
    pub rev_lookup: MovePathLookup<'tcx>,
    pub inits: IndexVec<InitIndex, Init>,
    // ...
}

MoveData 的核心是 MovePath 的树形结构。每个 MovePath 代表一个可能被移动的路径(place),例如 xx.fieldx.field.subfield

pub struct MovePath<'tcx> {
    pub next_sibling: Option<MovePathIndex>,
    pub first_child: Option<MovePathIndex>,
    pub parent: Option<MovePathIndex>,
    pub place: Place<'tcx>,
}

这种树形结构使编译器能精确追踪部分移动。例如对 struct Pair { first: String, second: String },MovePath 树为:

mp0: p (整体)
├── mp1: p.first
└── mp2: p.second

2.2.4 初始化状态的数据流分析

编译器使用两个互补的数据流分析追踪每个 MovePath 的初始化状态:

// 源码:compiler/rustc_mir_transform/src/elaborate_drops.rs
let move_data = MoveData::gather_moves(body, tcx, |ty| ty.needs_drop(tcx, typing_env));
let mut inits = MaybeInitializedPlaces::new(tcx, body, &env.move_data)
    .iterate_to_fixpoint(tcx, body, Some("elaborate_drops"))
    .into_results_cursor(body);
let uninits = MaybeUninitializedPlaces::new(tcx, body, &env.move_data)
    .iterate_to_fixpoint(tcx, body, Some("elaborate_drops"))
    .into_results_cursor(body);

注意过滤条件 |ty| ty.needs_drop(tcx, typing_env)——编译器只为需要析构的类型构建 MovePath。i32bool 等不需要析构的类型不追踪移动状态。

两个分析结果的组合决定了 Drop 策略:

maybe_init maybe_uninit DropStyle
false - Dead —— 不需要 Drop
true false Static —— 无条件 Drop
true true Conditional —— 需要 drop flag

2.3 Copy vs Move:编译器如何决策

2.3.1 Copy trait 的判定

flowchart TD
    A["类型 T"] --> B{"T 实现了<br/>Copy trait?"}
    B -->|"是"| C["MIR: Operand::Copy<br/>源变量仍有效"]
    B -->|"否"| D["MIR: Operand::Move<br/>源变量失效"]

    E["自动 Copy"] --> F["i32, f64, bool, char<br/>&T, 元组/数组(若所有元素 Copy)"]
    G["永远 Move"] --> H["String, Vec, Box<br/>&mut T, 实现了 Drop 的类型"]

    I["关键约束:Copy 和 Drop 互斥<br/>编译错误 E0184"] -.-> B

    style C fill:#10b981,color:#fff,stroke:none
    style D fill:#f59e0b,color:#fff,stroke:none
    style I fill:#ef4444,color:#fff,stroke:none

Copy 类型在 MIR 中使用 Operand::Copy

// Copy 类型的 MIR
_2 = _1;                  // Operand::Copy —— 没有 move 关键字
// _1 仍然有效

// Move 类型的 MIR
_2 = move _1;             // Operand::Move
// _1 变为未初始化

2.3.2 Copy 和 Drop 互斥(E0184)

#[derive(Copy, Clone)]
struct Foo;
impl Drop for Foo { fn drop(&mut self) {} }
// error[E0184]: the trait `Copy` cannot be implemented for this type;
// the type has a destructor

原因直观:如果允许同时实现,let b = a; 执行 Copy 后,ab 都有效,函数结束时两者都执行 Drop——double free。

2.3.3 needs_drop:编译器的类型分析

Ty::needs_drop()compiler/rustc_middle/src/ty/util.rs 中递归分析类型是否需要析构:

// 源码:compiler/rustc_middle/src/ty/util.rs
pub fn needs_drop_components_with_async<'tcx>(
    tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, asyncness: Asyncness,
) -> Result<SmallVec<[Ty<'tcx>; 2]>, AlwaysRequiresDrop> {
    match *ty.kind() {
        // 基本类型永远不需要 Drop
        ty::Bool | ty::Int(_) | ty::Uint(_) | ty::Float(_)
        | ty::FnPtr(..) | ty::Char | ty::RawPtr(..) | ty::Ref(..) => Ok(SmallVec::new()),
        // 动态类型总是需要 Drop
        ty::Dynamic(..) => Err(AlwaysRequiresDrop),
        // 数组:元素需要 Drop 且长度非零 → 需要 Drop
        ty::Array(elem_ty, size) => { /* 递归检查 */ },
        // 元组:任何字段需要 Drop → 整体需要 Drop
        ty::Tuple(fields) => { /* 递归检查 */ },
        // ADT、泛型等:需要进一步查询
        ty::Adt(..) | ty::Param(_) | ty::Closure(..) => Ok(smallvec![ty]),
    }
}

这个分析的结果直接决定编译器是否为某个类型构建 MovePath、是否插入 Drop 终止符。

2.4 Drop 析构:编译器何时、如何插入析构代码

2.4.1 MIR 阶段的语义变化

MirPhase::Analysis(drop elaboration 之前),Drop 终止符是条件性的——表示"如果这个值已初始化就析构"。在 MirPhase::Runtime(之后),所有 Drop 变为无条件的。

编译器源码中的注释明确说明了这一点:

"In analysis MIR, Drop terminators represent conditional drops... In runtime MIR, the drops are unconditional; when a Drop terminator is reached, if the type has drop glue that drop glue is always executed."

2.4.2 Drop Elaboration Pass

Drop elaboration 定义在 compiler/rustc_mir_transform/src/elaborate_drops.rs 中:

// 源码:compiler/rustc_mir_transform/src/elaborate_drops.rs
/// At a high level, this pass refines Drop to only run the destructor if the
/// target is initialized. The way this is achieved is by inserting drop flags
/// for every variable that may be dropped, and then using those flags to
/// determine whether a destructor should run.
pub(super) struct ElaborateDrops;

执行流程:

ElaborateDrops::run_pass()
├── MoveData::gather_moves()         构建 MovePath 树
├── MaybeInitialized/Uninit 分析     数据流不动点计算
├── compute_dead_unwinds()           识别不可达 unwind 边
└── ElaborateDropsCtxt::elaborate()
    ├── collect_drop_flags()         收集需要 drop flag 的路径
    ├── elaborate_drops()            精化每个 Drop 终止符
    └── drop_flags_on_init/args/locs 设置 drop flag 初始值和更新逻辑

2.4.3 四种 DropStyle

编译器为每个 Drop 确定一种风格,定义在 compiler/rustc_mir_transform/src/elaborate_drop.rs 中:

pub(crate) enum DropStyle {
    Dead,         // 所有路径上都未初始化 → 不执行 Drop
    Static,       // 所有路径上都已初始化 → 无条件 Drop
    Conditional,  // 状态取决于控制流 → 需要 drop flag
    Open,         // 部分移动 → 只 Drop 仍初始化的子字段
}

对应的判定逻辑:

fn drop_style(&self, path: Self::Path, mode: DropFlagMode) -> DropStyle {
    // ...
    match (maybe_init, maybe_uninit, multipart) {
        (false, _, _) => DropStyle::Dead,
        (true, false, _) => DropStyle::Static,
        (true, true, false) => DropStyle::Conditional,
        (true, true, true) => DropStyle::Open,
    }
}

DeadStatic 是最常见情况——编译期完全确定,无需运行时判断。

2.4.4 Drop Elaboration 完整示例

fn conditional_drop(condition: bool) {
    let s = String::from("hello");
    if condition {
        drop(s);       // 显式 drop
    }
    // s 是否已被 drop?取决于 condition
}

Drop elaboration 之前,bb2(函数出口)的 drop(s)condition=true 时会 double free。数据流分析发现 bb2s 同时 maybe_initmaybe_uninit,于是确定为 Conditional 风格。

elaboration 之后的 MIR:

bb0: {
    _2 = String::from("hello");
    _3 = const true;              // drop flag 初始化为 true
    switchInt(_1) -> [0: bb2, otherwise: bb1];
}
bb1: {                            // condition == true
    drop(_2);                     // Static drop
    _3 = const false;             // drop flag = false
    goto -> bb2;
}
bb2: {
    switchInt(_3) -> [0: bb4, otherwise: bb3];  // 检查 drop flag
}
bb3: { drop(_2); goto -> bb4; }   // Conditional drop
bb4: { return; }

2.5 Drop Flag:运行时追踪移动状态

2.5.1 创建条件

Drop flag 只在数据流分析表明某路径同时 maybe_initmaybe_uninit 时创建:

// 源码:compiler/rustc_mir_transform/src/elaborate_drops.rs
fn collect_drop_flags(&mut self) {
    for (bb, data) in self.body.basic_blocks.iter_enumerated() {
        // ... 对每个 Drop 终止符
        on_all_children_bits(self.move_data(), path, |child| {
            let (maybe_init, maybe_uninit) = self.init_data.maybe_init_uninit(child);
            if maybe_init && maybe_uninit {
                self.create_drop_flag(child, terminator.source_info.span)
            }
        });
    }
}

fn create_drop_flag(&mut self, index: MovePathIndex, span: Span) {
    self.drop_flags[index].get_or_insert_with(||
        self.patch.new_temp(self.tcx.types.bool, span)  // 创建 bool 局部变量
    );
}

2.5.2 生命周期

  1. 函数入口:所有 drop flag 初始化为 false
  2. 变量初始化时:设为 true
  3. 值被移走时:设为 false
  4. Drop 点:检查 flag,为 true 则执行 Drop

2.5.3 优化

直线代码中 drop flag 的值在编译期完全可知,LLVM 的常量传播和死代码消除会将其完全移除。只有条件分支中的 drop(if cond { drop(x); })、循环中的条件移动等场景才需要保留运行时 drop flag。

保留时的代价极小:1 字节栈空间 + 一次条件跳转,与 C++ unique_ptr 析构时的 null 检查本质相同。

2.5.4 elaborate_drops 的三条快捷路径和一个 panic 恢复矩阵

上面讲了 drop flag 什么时候被创建。但 collect_drop_flags 的对应函数 elaborate_dropsrustc_mir_transform/src/elaborate_drops.rs:335)才是把 TerminatorKind::Drop 最终变成实际执行代码的地方——这段代码比表面上复杂得多:

快捷路径一:needs_drop 直接删除(line 348-355):

if !place.ty(&self.body.local_decls, self.tcx)
        .ty.needs_drop(self.tcx, self.typing_env()) {
    self.patch.patch_terminator(bb, TerminatorKind::Goto { target });
    continue;
}

如果被 drop 的值的类型不需要 dropi32&TCopy 类型等),TerminatorKind::Drop 直接换成 TerminatorKind::Goto——drop 调用彻底从 MIR 消失。这比上一节说的 "LLVM 常量传播会把它移除" 早得多——在 MIR 阶段就砍掉,LLVM 根本不会看到这些无用 drop。对纯值类型(Vec<i32> 里 drop 单独的 i32 元素)累积能省下巨量 IR 体积。

panic 恢复矩阵(line 360-375)——drop 本身可能 panic,要给它预备好"这次 drop 挂了接下来去哪"的路由:

let unwind = match unwind {
    _ if data.is_cleanup => Unwind::InCleanup,
    UnwindAction::Cleanup(cleanup) => Unwind::To(cleanup),
    UnwindAction::Continue => Unwind::To(self.patch.resume_block()),
    UnwindAction::Unreachable => Unwind::To(self.patch.unreachable_cleanup_block()),
    UnwindAction::Terminate(reason) => {
        debug_assert_ne!(reason, UnwindTerminateReason::InCleanup,
            "we are not in a cleanup block, InCleanup reason should be impossible");
        Unwind::To(self.patch.terminate_block(reason))
    }
};

五种 unwind 路由 对应"drop 自己 panic 时"的五种处理策略:

最后那个 debug_assert_ne! 很有意思——它声明"非 cleanup block 里不应该出现 InCleanup 原因",如果真出现是编译器 bug。这种在不变式本应成立的地方加 assertion 捕获 rustc 自己的 bug 是 rustc 源码的常见防御性编程。

LookupResult::Parent(Some(_)) 处理(line 388-400)——被 drop 的 place 不是直接 tracked 而是通过父路径 tracked(比如 *boxarray[i] 这种 deref 后的位置)。这种情况只在 replace=true 时允许("drop 前重新赋值" 场景)、否则直接 span_bug(表示 rustc bug 不是用户代码错)。借用检查器会保证这种位置在赋值前已经初始化、所以可以无条件 drop 不需要 flag。这解释了为什么 *box = new_value 不会因为"旧值是否已 drop"而需要运行时检查。

async_fut: _(line 339)——TerminatorKind::Drop 有一个 async_fut 字段支持 async drop。2024 年才开始实验性稳定的 async Drop 特性就在这里和普通 drop 分道扬镳——async drop 生成 future 而不是直接调用。目前本函数在这里用 _ 忽略(直接走同步路径),未来 async drop 稳定后会在这里加分支。这也是代码里为未来特性预留接口点的典型手法——先在数据结构里开洞、功能逐步填进来。

读完 elaborate_drops 你会发现所谓 "Drop 很简单" 是错觉——简单的是用户视角的语义(离开作用域就析构);难的是编译器视角的实现(和 unwind 协议、异步、type-level 需求分析、MoveData 索引、cleanup block 网络错综交织)。这段 400 行代码是 Rust 异常安全的基石——每一行都在回答一个"如果 panic 了怎么办"的具体问题。

2.6 析构顺序:编译器的确定性保证

2.6.1 规则

类别 析构顺序
局部变量 声明逆序
结构体字段 声明正序
元组/数组元素 索引正序
graph TD
    A["函数入口"] --> B["StorageLive(_1) // first"]
    B --> C["StorageLive(_2) // second"]
    C --> D["StorageLive(_3) // third"]
    D --> E["... 函数体 ..."]
    E --> F["drop(_3) // third 最先析构"]
    F --> G["drop(_2) // second 其次"]
    G --> H["drop(_1) // first 最后"]
    H --> I["return"]

    style F fill:#ef4444,color:#fff,stroke:none
    style G fill:#f59e0b,color:#fff,stroke:none
    style H fill:#3b82f6,color:#fff,stroke:none

2.6.2 为什么逆序很重要

逆序析构保证后声明的变量(可能引用先声明的变量)先被析构,避免悬垂引用:

fn why_reverse_order() {
    let data = vec![1, 2, 3];       // 先声明
    let reference = &data;           // 后声明,引用 data
    // 逆序析构:reference 先释放,data 后释放 → 安全
    // 如果正序:data 先释放,reference 变成悬垂引用 → 不安全
}

对锁守卫尤为关键:

fn lock_order() {
    let guard_a = mutex_a.lock().unwrap();  // 先获取
    let guard_b = mutex_b.lock().unwrap();  // 后获取
    // 析构:guard_b → guard_a(先释放后获取的锁,与获取顺序相反)
    // 这是避免死锁的最佳实践
}

2.7 部分移动:结构体的所有权碎片化

2.7.1 机制

struct Pair { first: String, second: String }

fn partial_move() {
    let p = Pair { first: "hello".into(), second: "world".into() };
    let f = p.first;           // 部分移动
    // p.first → 不可用(已移出)
    // p.second → 可用
    // p 整体 → 不可用(部分初始化)
    println!("{}", p.second);  // OK
}

编译器为每个字段独立追踪初始化状态。函数结束时只对 p.second 调用 Drop,这就是 DropStyle::Open 的用途——"打开"结构体,只 Drop 仍初始化的字段。

2.7.2 限制

实现了 Drop 的类型不允许部分移动——因为 drop(&mut self) 需要访问完整结构体,如果某字段已被移出,析构函数就会访问未初始化内存。引用背后的值也不允许部分移动,因为引用不拥有所有权。

2.8 借用检查器与所有权的关系

借用检查器建立在所有权系统之上。所有权回答"谁负责析构",借用检查器回答"谁可以在什么时候访问"。两者共享同一套 MoveData 基础设施:

// 源码:compiler/rustc_borrowck/src/lib.rs
use rustc_mir_dataflow::move_paths::{MoveData, MovePathIndex};
use rustc_mir_dataflow::impls::{EverInitializedPlaces, MaybeUninitializedPlaces};

借用检查器用 MoveData 检测三类错误:use after move、move while borrowed、use of uninitialized value。这种复用体现了编译器的设计哲学——所有权和借用是同一问题的两个面。

2.9 所有权的 MIR 表示:完整视图

2.9.1 MIR 阶段与所有权语义

graph LR
    A["MirPhase::Built"] --> B["MirPhase::Analysis"]
    B --> C["MirPhase::Runtime"]

    A1["Drop 是条件性的<br/>Copy 仅限 Copy 类型"] --> A
    B1["借用检查在此执行<br/>Move 错误在此报告"] --> B
    C1["Drop elaboration 完成<br/>Drop 变为无条件的<br/>drop flag 已插入"] --> C

    style A fill:#3b82f6,color:#fff,stroke:none
    style B fill:#f59e0b,color:#fff,stroke:none
    style C fill:#10b981,color:#fff,stroke:none

到了 MirPhase::RuntimeOperand::Copy 不再受 Copy trait 限制——因为所有析构决策已固化为显式 Drop 和 drop flag。

2.9.2 StorageLive/StorageDead vs Drop

StorageLive/StorageDead 管理栈空间的分配释放,Drop 管理值的析构(如释放堆内存)。两者是不同层次:

StorageLive(s)  → 栈上分配 24 字节(String 的 ptr + len + cap)
s = String::from("hello")  → 堆上分配 5 字节
drop(s)         → 释放堆上的 5 字节
StorageDead(s)  → 释放栈上的 24 字节

2.9.3 DropFlagMode:浅层 vs 深层

// 源码:compiler/rustc_mir_transform/src/elaborate_drop.rs
pub(crate) enum DropFlagMode {
    Shallow,  // 只影响顶层 drop flag
    Deep,     // 影响所有嵌套子字段的 drop flag
}

Shallow 用于简单赋值,Deep 用于 Drop——析构一个值时,所有子字段的 drop flag 都需要清除。

2.10 与其他内存模型的对比

graph TB
    subgraph "手动管理 - C"
        C1["malloc/free<br/>⚠️ 泄漏/double free/UAF"]
    end
    subgraph "垃圾回收 - Java/Go"
        G1["运行时 GC<br/>⚠️ 暂停、内存占用高"]
    end
    subgraph "引用计数 - Swift ARC"
        A1["retain/release<br/>⚠️ 原子操作开销、循环引用"]
    end
    subgraph "所有权 - Rust"
        R1["编译期追踪<br/>零运行时开销、编译期安全"]
    end

    style C1 fill:#ef4444,color:#fff,stroke:none
    style G1 fill:#f59e0b,color:#fff,stroke:none
    style A1 fill:#f59e0b,color:#fff,stroke:none
    style R1 fill:#10b981,color:#fff,stroke:none
维度 C (手动) Java/Go (GC) Swift (ARC) Rust (所有权)
安全保证 运行时 部分 编译期
运行时开销 零(但易出错) GC 暂停 原子引用计数
析构时机 手动 不确定 确定性 确定性
并发安全 程序员负责 GC 处理 原子操作 编译期 Send/Sync
适用场景 嵌入式/底层 应用层服务 iOS/macOS 系统编程/高性能

C 的手动管理:编译器不提供任何安全网。char *t = s; free(s); printf("%s", t); 是合法的 C 代码,但会导致 use after free。Rust 的所有权系统在编译期阻止所有此类错误。

Java/Go 的 GC:消除了手动管理错误,但析构时机不确定(finalize() 可能永远不被调用),且 GC 暂停对实时系统不可接受。Rust 既有确定性析构又无 GC 暂停。

Swift 的 ARC:有确定性析构,但引用计数的原子操作有性能开销,循环引用会泄漏。Rust 的 Rc<T>/Arc<T> 是可选的,大多数代码用纯所有权模型,无引用计数开销。

2.11 常见所有权模式

2.11.1 Builder 模式

利用所有权转移实现类型安全的方法链:

struct QueryBuilder { table: String, conditions: Vec<String>, limit: Option<usize> }

impl QueryBuilder {
    fn new(table: &str) -> Self {
        QueryBuilder { table: table.into(), conditions: vec![], limit: None }
    }
    fn where_clause(mut self, cond: &str) -> Self {  // 消费 self
        self.conditions.push(cond.into()); self
    }
    fn limit(mut self, n: usize) -> Self { self.limit = Some(n); self }
    fn build(self) -> String {  // 消费 self → 之后不可再用
        format!("SELECT * FROM {} WHERE {} LIMIT {:?}",
            self.table, self.conditions.join(" AND "), self.limit)
    }
}

每个方法调用在 MIR 中都是 Operand::Move——self 移入方法,返回值移回调用者。build() 消费 self 后,编译器保证不会被误用。

2.11.2 RAII 模式

所有权绑定资源生命周期——即使 panic 也能正确清理:

struct Transaction { committed: bool, /* ... */ }

impl Transaction {
    fn commit(mut self) -> Result<(), Error> {  // 消费 self
        self.committed = true;
        Ok(())
    }
}

impl Drop for Transaction {
    fn drop(&mut self) {
        if !self.committed {
            eprintln!("Transaction not committed, rolling back");
        }
    }
}

commit 消费 self——提交后不能再操作(编译期保证)。未提交时 Drop 自动回滚。

2.11.3 类型状态模式

用所有权转移编码状态机,使非法状态转换成为编译错误:

struct Disconnected;
struct Connected { stream: TcpStream }
struct Authenticated { stream: TcpStream, token: String }

impl Disconnected {
    fn connect(self, addr: &str) -> Result<Connected, io::Error> {
        Ok(Connected { stream: TcpStream::connect(addr)? })
    }  // Disconnected 被消费,不能再使用
}

impl Connected {
    fn authenticate(self, creds: &str) -> Result<Authenticated, Connected> {
        // 验证成功:所有权从 Connected 转移到 Authenticated
        // 验证失败:所有权返回 Connected
    }
}

impl Authenticated {
    fn send_data(&mut self, data: &[u8]) -> Result<(), io::Error> {
        // 只有认证后才能发送——编译期保证
    }
}

2.11.4 Newtype 模式

所有权封装创建类型安全抽象,零运行时开销:

struct UserId(String);
struct Email(String);

// 编译错误:UserId 不是 Email,类型不匹配
// send_email(user_id);

// 内存布局与裸 String 完全相同(#[repr(transparent)] 语义)
// 所有权操作零额外开销

2.12 本章小结

本章从编译器源码层面剖析了所有权系统的实现:

下一章我们将深入借用检查器——它建立在本章的 MIR 和初始化状态追踪之上,用 NLL 算法判断引用的合法性。

2.13 源码实证:rustc_mir_dataflow/src/move_paths/mod.rs 442 行

本章讲了 MovePath——打开 compiler/rustc_mir_dataflow/src/move_paths/mod.rs:58-64你会看到 MovePath 的实际定义

#[derive(Clone)]
pub struct MovePath<'tcx> {
    pub next_sibling: Option<MovePathIndex>,
    pub first_child: Option<MovePathIndex>,
    pub parent: Option<MovePathIndex>,
    pub place: Place<'tcx>,
}

四个字段讲一个故事——

为什么用 Option<MovePathIndex> 而不是 Vec<MovePathIndex>——index-based 的兄弟链Vec<Index> 省内存——每个 MovePath 只占 4 个字段 × 8 bytes = 32 B——1M 个 MovePath 只占 32 MB——rustc 的内存预算紧

对比 naive 实现——Vec<Index> 动态分配 + heap 碎片——rustc 处理大型 crate 时内存会爆

这段数据结构选择——是 rustc 无数次性能优化后凝练的——不是设计课本里的"标准答案"、"编译器工程经验"的结晶

2.14 MovePath 为什么用 index 不用指针

MovePathIndex 本质是 u32 索引——不是裸指针、不是 &MovePath——三个好处

好处 1——克隆便宜——MovePath 树本身不 Clone(因为有循环引用)、MovePathIndexCopy、到处传递零成本

好处 2——borrow checker 友好——&move_paths[mpi] 是每次 lookup 即借即还——同一 MoveData 可以在多个位置被读取不会触发借用冲突

好处 3——序列化 / cache 友好——MovePathIndex 可以直接存到 incremental compilation cache——不涉及指针修复——加速 cargo check

Rust 编译器内部大量使用这种"Vec + Index newtype"模式——LocalDefIdBasicBlockIdxHirIdNodeId——全是 u32 包装——这是 rustc 的 idiom

读者写自己的 compiler-like 系统时——learn this pattern——Arc<Mutex<HashMap<NodeId, Arc<Node>>>> 效率高一个数量级

2.15 find_descendant 的 BFS 实现——工程细节

mod.rs:97-126find_descendant——用 BFS 找满足条件的第一个后代

pub fn find_descendant(
    &self,
    move_paths: &IndexSlice<MovePathIndex, MovePath<'_>>,
    f: impl Fn(MovePathIndex) -> bool,
) -> Option<MovePathIndex> {
    let Some(child) = self.first_child else { return None };
    let mut todo = vec![child];
    while let Some(mpi) = todo.pop() {
        if f(mpi) { return Some(mpi); }
        let move_path = &move_paths[mpi];
        if let Some(child) = move_path.first_child { todo.push(child); }
        if let Some(sibling) = move_path.next_sibling { todo.push(sibling); }
    }
    None
}

几个值得学的技巧——

这一个方法里藏着 rustc 编译器的编码风格——let-else + Vec+pop + impl Fn 是 2024 年 rustc 代码的标准范式

2.16 builder.rs 593 行:从 MIR 构造 MovePath 树

compiler/rustc_mir_dataflow/src/move_paths/builder.rs 593 行——编译器启动时的关键 pass

核心流程——

593 行看似多——但每一行都有明确用途——MIR 的所有 Statement 类型都要处理(Assign / StorageLive / StorageDead / Drop / ...)——没偷懒

对本书读者的启发——如果你写静态分析器、IR 解析器、"path tree + place lookup"是成熟范式——不要自己发明新轮子

2.17 BorrowSetMoveData 的分工

本章讲 MoveData——rustc 还有一个孪生的 BorrowSetrustc_borrowck/src/borrow_set.rs):

两者配合——

这是"两个数据结构 + 一套数据流分析"的协作——rustc 把"所有权" "借用" 解耦——各自可读、组合可验

2.18 Rust 相比 C++ 的"静态析构"优势

本章§2.9 对比过内存模型——这里再深挖"静态析构" 一条

C++——

Rust——

工程后果——

这是 Rust 的"杀手锏"——不是"比 C++ 安全"(C++ 用好了也安全),而是"安全 + 可预测"——两者兼得、稀缺

2.19 needs_drop() 函数——编译器的"懒惰判断"

rustc 有个内部函数 TyCtxt::needs_drop(ty)——判断一个类型是否需要析构

判断规则——

优化意义——

实测——一个 struct Point { x: f64, y: f64 } 的变量——离开 scope 时生成 0 条指令——因为 needs_drop::<Point>() == false

这是 Rust "零开销抽象" 的具体落地——抽象层次高、但生成的代码跟手写 C 一样紧

2.20 Rust 2024 edition 的"生命周期推导改进"

Rust 2024(2024-11 发布)对所有权和生命周期做了几个小但重要的改进——

改进 1:match ergonomics 修复——以前 match &opt { Some(x) => ...}x 类型推导有坑——2024 修复为更直觉的版本

改进 2:if let scope extension——if let Some(x) = foo { ... } else { } 的 x 作用域延伸到 else——更顺

改进 3:temporary lifetime 调整——let _x = vec![1, 2, 3].first() 的临时 vec 寿命"稍微"改变——边界 case 行为变清晰

改进 4:captured variables in closures——以前 move || x.field 会 move 整个 x、现在只 move x.field——解决"只用一个字段却拿全所有权"的历史坑

这四条改动——看似小、但去除了无数"我的代码应该能编但不能"的历史疑难杂症——Rust 2024 用户反馈普遍正面

2.21 所有权 × 生命周期 × 借用——三角关系

本章讲所有权——但 Rust 的安全其实是"所有权 + 借用 + 生命周期" 三位一体

三者缺一不可——

Rust 的精彩之处——三者都有、互相正交、又互相完备——构成一个"形式化可验证"的内存安全模型

这个模型 2010 年 Graydon Hoare 开始设计——2015 年 1.0 稳定——花了 5 年——业内公认"最接近 Haskell 类型安全 + 接近 C 性能" 的语言

2.23 rustc_borrowck 的模块结构

本章多次呼应下章——这里先给读者一张地图

compiler/rustc_borrowck/src/ 目录里的关键文件——

文件 职责
lib.rs 入口:mir_borrowck 主函数
borrow_set.rs BorrowSet 数据结构
nll.rs Non-Lexical Lifetimes 算法
places_conflict.rs 判断两个 place 是否冲突
constraints/ 生命周期约束图
polonius/ 新一代借用检查(实验)
diagnostics/ 借用错误的诊断信息
dataflow.rs 借用状态流分析

10+ 个子模块、~15000 行代码——借用检查是 rustc 最复杂的模块之一

本章讲所有权(rustc_mir_dataflow 模块)——下章讲借用检查(rustc_borrowck——两章之间紧密咬合

2.24 Polonius 的未来——更精确的借用检查

Rust 2019 起在开发 Polonius——基于 Datalog 的下一代借用检查器

Polonius 相比 NLL 的优势——

但 Polonius 慢——Datalog 引擎对大 crate 编译时间翻倍——至今仍在 nightly 实验中

2026 年状态——Polonius 可选 -Z polonius=next 打开仅用于诊断(检出可能是借用 bug 的代码但不改变主流程)——完全替代 NLL 还需 2-3 年

读者为何要关心——Polonius 代表 Rust 借用检查的未来——你写今天的代码、未来可能被 Polonius 重新检查——某些"NLL 通过但 Polonius 拒绝"的模式要注意避开

2.25 所有权 + unsafe 的**"边界"

Rust 的安全边界由所有权 + 借用 + 生命周期构成——unsafe block 是"逃生出口":

unsafe 能做什么——

unsafe 不能做什么——

编写 unsafe 的黄金法则——

这条"safe + unsafe 两层"设计——是 Rust 工程实用主义的体现——100% safe 做不出 OS kernel / 高性能运行时——留个 unsafe 口子、但严格管控

2.27 所有权的**"常见新手坑"**十条

即便学完本章理论——实操时仍然会栽——十条典型坑

  1. for 循环里消费 Vec 还想继续用——for x in v { ... } 之后 v 已被 move——&vv.iter() 借用
  2. &mut self 暴露给两个地方——不会编译——一处借用时另一处必须等
  3. struct 里持有 &T 字段——需要生命周期参数 struct Foo<'a> { data: &'a T }——省不掉
  4. 闭包意外 move 外部变量——move || 强制 move 或"引用 capture" 保留借用
  5. 递归数据结构——struct Node { next: Node } 编译不过(无限大小)——Box<Node>
  6. Vec + push 后想借用之前的元素——push 可能扩容导致旧引用失效——先借用再 push 不行
  7. String + &str——&str 是借用String 是所有——函数参数通常用 &str(接受两者)
  8. Option::take / mem::replace——&mut T"取走" ——标准技巧、不是 hack
  9. Rc/Arc 循环——Rust 不检测——Weak 打破循环、否则 memory leak
  10. HashMap entry API——map.entry(key).or_insert_with(|| ...)if !contains + insert 更符合借用规则

这 10 条——每条都有几十个 Stack Overflow 问答——预先知道能节省 10-20 小时 debug

2.28 所有权的**"哲学映射"

Rust 的所有权本质是什么——三个映射

映射 1:到 C++ 的 RAII——Rust 把 C++ 的 RAII 原则变成"语言规则"——编译器强制而非开发者纪律

映射 2:到 Haskell 的 linear types——每个值只用一次——Rust 的所有权 + move 语义就是 linear type 的工业落地

映射 3:到"资源治理"**——除了内存、文件句柄、锁、数据库连接"所有权"都适用——Rust 的 RAII 覆盖一切资源

三个映射合起来——Rust 的所有权"既不是新发明、也不是老调重弹"、是把多个学术成果"工业化落地"的产物——这就是"伟大语言"的共性

2.30 Copy trait 的**"内部实现"**——一行代码的大秘密

Copy 是 Rust 里最简单的 trait——trait body 完全为空

pub trait Copy: Clone {}

空的!——没有方法、没有关联类型、没有 trait objects

但它对 rustc 意义重大——

空 trait 的用法——"marker trait"——不提供行为、只提供"类型 tag"——rustc 编译时查这个 tag 做特殊处理

marker trait 在 Rust 里还有——SendSyncSizedUnpinFreeze——每一个都是"语言级能力" 的开关

这种"空 trait + 编译器特殊处理"的设计——"大量关键字" 更灵活——语言层面只有一个"trait" 概念、各种标记都复用它

2.31 Drop trait 的**"反直觉约束"

Drop 是 Rust 里最特殊的 trait——三个反直觉约束

约束 1:不能手动调用 .drop()——用户写 x.drop() 会被 rustc 拒绝——只能让 rustc 在 scope 结束时自动调

为什么——如果用户手动调完rustc 在 scope 结束时又自动调一次 → double free——rustc 强制"只能自动调用"。

约束 2:DropCopy 互斥(E0184)——一个类型不能同时 impl Dropimpl Copy

为什么——Copy 意味着"bit-wise 复制、无副作用"、Drop 意味着"析构时有副作用"——两者逻辑冲突

约束 3:Drop::drop(&mut self)——&mut self 而不是 self——drop 里还有 self、但之后整个 self 被回收——&mut self"最后一次修改机会"。

经典用法——MutexGuard::drop释放锁File::drop关闭 file descriptor——都用 &mut self

2.33 ManuallyDrop 的用途——**"禁用自动析构"

Rust 标准库有个 std::mem::ManuallyDrop<T>——包一层阻止自动析构

use std::mem::ManuallyDrop;
let x: ManuallyDrop<String> = ManuallyDrop::new(String::from("hi"));
// x 离开 scope 时,x.inner 不会被 drop

为什么需要这个——

危险——如果不手动调 ManuallyDrop::drop、就是"内存泄漏"——不 unsafe、但逻辑错误

应用——Vec::with_capacity 内部用它、MaybeUninit<T> 也用它——所有"手动控制生命周期"的底层原语

2.34 MaybeUninit<T>——**"未初始化"**的类型

Rust 不允许**"读一个未初始化变量"——这是安全的根基——但底层代码有时需要

use std::mem::MaybeUninit;
let mut x: MaybeUninit<String> = MaybeUninit::uninit();
// x 里现在是垃圾、读 x.assume_init() 是 UB
unsafe { x.as_mut_ptr().write(String::from("hello")); }
let s: String = unsafe { x.assume_init() };  // 现在合法

使用场景——

MaybeUninit 是 Rust 1.36 新增——替代了"mem::uninitialized()" 的老 API(老 API 立即 UB、新 API 类型级别明确可能未初始化)——类型系统进步的典范

2.36 Pin<T> 与自引用 struct——所有权的**"固定版"

所有权一般假设"可移动"——但 async / self-referential struct 要求"不可移动"——Rust 用 Pin<T> 解决

use std::pin::Pin;
use std::marker::PhantomPinned;

struct SelfRef {
    data: String,
    ptr: *const String,  // 指向自己的 data
    _pin: PhantomPinned,
}

Pin<P> 的本质——"保证 P 指向的数据不会被 move"——即使你有 &mut P、也不能 mem::swap

为什么需要——

使用 Pin 的两种方式——

Pin 是 Rust 所有权的"高阶扩展"——本章未深入但值得读者知道存在——写 async 库必须理解它

2.37 SendSync跨线程所有权

所有权规则在单线程内讨论过——跨线程场景由 Send / Sync 补全

Rust 的跨线程安全"完全编译时保证"——不是"运行时 lock 判断"。

几个典型类型——

这就是 Rust 的 "fearless concurrency"——类型系统强制"不 Send 的东西别跨线程"——编译期杜绝数据竞争

2.39 实战:5 个"重写为 Rust 式所有权"**的真实练习

读者最好的检验方式——做这 5 个练习

练习 1——把一段 C++ unique_ptr<Foo> 代码翻译成 Rust Box<Foo>——对比"所有权转移时的语法"**。

练习 2——用 Rust 实现 LinkedList<T>——先写不出来(会卡 borrow checker)、查资料了解"Option<Box<Node>> + Option<Rc<RefCell<Node>>>" 两种解法

练习 3——把一个"回调模式"(如事件总线)重写——Box<dyn Fn> + Vec<Box<dyn Fn>>——理解 trait object 的所有权

练习 4——写一个"带析构日志"的 struct——impl Dropprintln!——观察析构顺序(§2.9 讲过逆序)**。

练习 5——读一段 std::sync::Arc 源码——clone() 的原子 fetch_adddrop()fetch_sub——理解 reference counting 的内部

5 个练习做完——你对所有权不再是"书本知识"、"肌肉记忆"。

2.41 附录 A:10 个"不写代码就能记住的"所有权速查

10 条背下来——你写 Rust 不再卡借用检查

2.42 附录 B:三本其他书的呼应

三章合读、rustc 编译器的"内核知识图"完整

2.46 所有权相关的 8 个 std 工具

trick 1——mem::take(&mut x)——**"拿走值、留个默认值"——不需要持有 T: Copy——常用于"从 struct 里 take 一个字段但不想 consume struct"。

trick 2——Option::take(&mut self)——专门为 Option 设计、Some(x)None 并返回 x——替代 mem::take 针对 Option 场景

trick 3——std::mem::swap(&mut a, &mut b)——两个值互换所有权——不需要复制、不需要 clone

trick 4——Box::leak(b)——**"故意泄漏"得到 &'static T——用于"构造一次用永远不 drop" 的全局资源

trick 5——Cow<'a, T>(Clone-on-Write)——**"借用或拥有"二选一的智能类型——适合"大多数情况只借用、偶尔需要修改才 clone" 的场景

trick 6——Pin<Box<T>> + PhantomPinned——§2.36 讲过、构造自引用 struct

trick 7——_ = x——显式 drop x、等效于立即 drop(x)——drop(x) 更 idiomatic

trick 8——impl Trait 返回类型——不用写 box、不用写 dyn、编译器自动 monomorphize——**"静态多态" 的语法糖

这 8 个 API 覆盖了 std 里"所有权操纵"的主要入口——读 tokio / hyper / serde 源码时会反复看到。

2.48 所有权视角的四句话总结

"所有权不是规则、是思考方式"——用所有权的视角看代码你会发现:

这不是 Rust 独有——其他语言的好代码也在做同样的事、只是Rust 把**"好代码的习惯"变成了"语言规则"。

2.49 **"Clippy lint 和所有权"**的日常工作流

写 Rust 代码不用手工查所有权问题——cargo clippy 替你查

和所有权相关的 Clippy lint——

日常工作流——

Clippy 的价值——把本章讲的"理论知识"变成"自动化检查"——读者省几年"踩坑摸索"。

2026 年状态——Clippy 默认开启"500+ 条 lint"——覆盖度接近"静态分析器" 级别

2.51 最后附:一张**"所有权生命周期"**全景图

把本章所有概念压缩成一张"ASCII 流程图"——

let x = String::from("hi")           // [x: OWNED]
let y = &x                            // [x: OWNED + borrowed immutably]
drop(y)                               // [x: OWNED]
let z = &mut x                        // [x: OWNED + borrowed mutably]
*z = String::from("hi2")              // [x: still OWNED, z modifies]
drop(z)                               // [x: OWNED]
let w = x                             // [x: MOVED] [w: OWNED]
println!("{}", x)                     // ERROR: x has been moved
// w goes out of scope → String dropped

每一行都对应本章讨论的一种状态转换——

这 9 行 = 所有权一生的全部——能背下来、能讲出每一步的 rustc 内部状态——你就"懂了"。