Appearance
第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,由Copytrait 判定 - Copy 和 Drop 互斥(E0184),同时实现会导致 double free
- Drop elaboration 是 MIR 的关键 pass:分析所有控制流路径,将条件性 Drop 转换为确定性 Drop,必要时插入 drop flag
- 部分移动(partial move)使结构体进入"部分初始化"状态,编译器为每个字段独立追踪
- 析构顺序严格确定:局部变量按声明逆序,结构体字段按声明正序
2.1 所有权模型:每个值有且仅有一个所有者
Rust 的所有权系统建立在三条规则之上:每个值都有一个所有者;同一时刻只能有一个所有者;当所有者离开作用域时值被丢弃。从编译器的视角看,这三条规则的核心目标只有一个:
保证每个拥有析构函数(Drop)的值在所有控制流路径上恰好被销毁一次。
不多不少。不销毁意味着资源泄漏,销毁两次意味着 double free。
rust
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 中的表示:
rust
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 的本质区别:
rust
// 源码: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 后源变量被禁止访问。
这就是所有权系统"零开销"的本质——所有权转移完全是编译器跟踪的静态信息,没有引用计数的原子操作,没有 GC 暂停。
2.2.3 MoveData:编译器的移动追踪基础设施
编译器通过 MoveData 结构追踪所有 move 操作,定义在 compiler/rustc_mir_dataflow/src/move_paths/mod.rs 中:
rust
// 源码: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),例如 x、x.field、x.field.subfield:
rust
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.second2.2.4 初始化状态的数据流分析
编译器使用两个互补的数据流分析追踪每个 MovePath 的初始化状态:
- MaybeInitializedPlaces:某个程序点上,某条路径上被初始化
- MaybeUninitializedPlaces:某个程序点上,某条路径上被移走
rust
// 源码: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。i32、bool 等不需要析构的类型不追踪移动状态。
两个分析结果的组合决定了 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 的判定
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)
rust
#[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 后,a 和 b 都有效,函数结束时两者都执行 Drop——double free。
2.3.3 needs_drop:编译器的类型分析
Ty::needs_drop() 在 compiler/rustc_middle/src/ty/util.rs 中递归分析类型是否需要析构:
rust
// 源码: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,
Dropterminators represent conditional drops... In runtime MIR, the drops are unconditional; when aDropterminator 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 中:
rust
// 源码: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 中:
rust
pub(crate) enum DropStyle {
Dead, // 所有路径上都未初始化 → 不执行 Drop
Static, // 所有路径上都已初始化 → 无条件 Drop
Conditional, // 状态取决于控制流 → 需要 drop flag
Open, // 部分移动 → 只 Drop 仍初始化的子字段
}对应的判定逻辑:
rust
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,
}
}Dead 和 Static 是最常见情况——编译期完全确定,无需运行时判断。
2.4.4 Drop Elaboration 完整示例
rust
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。数据流分析发现 bb2 处 s 同时 maybe_init 和 maybe_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_init 和 maybe_uninit 时创建:
rust
// 源码: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 生命周期
- 函数入口:所有 drop flag 初始化为
false - 变量初始化时:设为
true - 值被移走时:设为
false - Drop 点:检查 flag,为
true则执行 Drop
2.5.3 优化
直线代码中 drop flag 的值在编译期完全可知,LLVM 的常量传播和死代码消除会将其完全移除。只有条件分支中的 drop(if cond { drop(x); })、循环中的条件移动等场景才需要保留运行时 drop flag。
保留时的代价极小:1 字节栈空间 + 一次条件跳转,与 C++ unique_ptr 析构时的 null 检查本质相同。
2.6 析构顺序:编译器的确定性保证
2.6.1 规则
| 类别 | 析构顺序 |
|---|---|
| 局部变量 | 声明逆序 |
| 结构体字段 | 声明正序 |
| 元组/数组元素 | 索引正序 |
2.6.2 为什么逆序很重要
逆序析构保证后声明的变量(可能引用先声明的变量)先被析构,避免悬垂引用:
rust
fn why_reverse_order() {
let data = vec![1, 2, 3]; // 先声明
let reference = &data; // 后声明,引用 data
// 逆序析构:reference 先释放,data 后释放 → 安全
// 如果正序:data 先释放,reference 变成悬垂引用 → 不安全
}对锁守卫尤为关键:
rust
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 机制
rust
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 基础设施:
rust
// 源码: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 阶段与所有权语义
到了 MirPhase::Runtime,Operand::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 深层
rust
// 源码: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 与其他内存模型的对比
| 维度 | 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 模式
利用所有权转移实现类型安全的方法链:
rust
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 也能正确清理:
rust
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 类型状态模式
用所有权转移编码状态机,使非法状态转换成为编译错误:
rust
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 模式
所有权封装创建类型安全抽象,零运行时开销:
rust
struct UserId(String);
struct Email(String);
// 编译错误:UserId 不是 Email,类型不匹配
// send_email(user_id);
// 内存布局与裸 String 完全相同(#[repr(transparent)] 语义)
// 所有权操作零额外开销2.12 本章小结
本章从编译器源码层面剖析了所有权系统的实现:
- Move 语义:MIR 中的
Operand::Move,物理上是 memcpy,语义上标记源为未初始化。通过MoveData和MovePath树追踪。 - Copy vs Move:由
Copytrait 决定。Copy 和 Drop 互斥(E0184)。needs_drop()递归判断类型是否需要析构。 - Drop elaboration:MIR 关键 pass,使用双向数据流分析,将条件性 Drop 转为四种确定性风格(Dead/Static/Conditional/Open)。
- Drop flag:仅在控制流歧义时插入,是一个
bool局部变量,大多数情况被 LLVM 优化掉。 - 析构顺序:变量逆序、字段正序。逆序保证引用在被引用物之前失效。
- 内存模型对比:Rust 将安全成本从运行时转移到编译时——零开销、确定性析构、编译期保证。
下一章我们将深入借用检查器——它建立在本章的 MIR 和初始化状态追踪之上,用 NLL 算法判断引用的合法性。