Skip to content

所有权延申

本章是对所有权一章的深入补充,侧重于从底层机制的角度回答以下三个问题:

  1. 谁可以拥有所有权?
  2. 编译器如何在底层实现所有权的唯一性保证?
  3. 借用规则(引用互斥)的根本原因是什么?

一、谁可以拥有所有权

在 Rust 中,有三类实体可以充当"所有者":

1. 变量

这是最常见的形态。当你声明 let s = String::from("hello"); 时,变量 s 就直接拥有了该字符串值的所有权。

2. 数据结构

结构体(struct)、枚举(enum)或容器(如 VecHashMap)拥有其内部成员的所有权。例如,一个结构体字段如果是 String 类型,那么该结构体实例就拥有这个字符串,当结构体被销毁时,该字段也会随之被释放。

rust
struct User {
    name: String,  // User 实例拥有 name 字段的所有权
}

3. 函数参数

当值作为参数传递给函数(非引用传递)时,函数的作用域会接管该值的所有权,函数返回后若未将所有权传出,该值即被释放。

rust
fn greet(s: String) {       // s 获得传入值的所有权
    println!("hello, {}", s);
}                           // s 离开作用域,值被释放

二、所有权的底层机制

所有权不是运行时的某种实体,而是编译器在编译阶段对内存管理责任的静态追踪。

编译器账本

可以把编译器想象成一个记账员,它维护着一个"内存账本":

  • 登记: 声明变量时,编译器记录"这块内存地址归此变量管理"
  • 转移: 执行 let b = a; 时,编译器将账本中 a 对应的条目划转b,同时将 a 标记为无效
  • 注销: 作用域结束时(}),编译器自动在此处插入 drop 指令,由最后的合法所有者负责清理

通过这套静态账本,编译器在不增加任何运行时开销的情况下,保证了"任意时刻只有一个所有者"这一不变量。

赋值即移动

在其他语言中,赋值通常是复制指针(导致两个变量指向同一块内存)。Rust 的 Move 语义与此不同:

  • 执行 let b = a; 时,确实发生了栈上数据的浅拷贝(复制指针、长度、容量等字段给 b)
  • 关键动作: 编译器在账本上将 a 标记为无效,b 成为唯一的合法所有者
  • 堆上的真实数据完全没有移动

这种"废除前任,只留现任"的方式,确保了所有者的唯一性,也彻底避免了二次释放(double free)问题。

三、借用互斥的底层根因

所有权一章讲解了借用规则:

  • 要么有任意数量的不可变引用
  • 要么有唯一一个可变引用

但为什么要有这个限制?真正的根因是内存重分配(Reallocation)

内存重分配

StringVec 在堆上增长且当前空间不足时,Rust 会:

  1. 在堆上申请一块更大的新内存
  2. 将旧内存的数据完整搬移到新地址
  3. 更新栈上结构体(句柄)中的指针字段,指向新地址
  4. 释放旧内存

关键后果: 搬家后,所有持有旧地址的指针都会瞬间变成野指针(Dangling Pointer)

可变引用与不可变引用互斥

假设允许以下情况同时存在:

rust
let mut s = String::from("hello");
let r1 = &s;          // 不可变引用,持有旧地址 0xA000
let r2 = &mut s;      // 可变引用,拥有修改权
r2.push_str("world"); // 触发扩容,String 搬到新地址 0xB000
println!("{}", r1);   // ❌ r1 仍指向旧地址 0xA000,访问已释放内存
  • r2 扩容后,s 的栈上指针已更新为 0xB000
  • r1 仍然持有旧地址 0xA000,读取时会访问已释放的内存,造成崩溃或安全漏洞

结论: 不可变引用要求数据"原地不动",而可变引用拥有"让数据搬家"的权力,两者在物理层面无法并存。

多个可变引用互斥

rust
let mut s = String::from("hello");
let r1 = &mut s;  // r1 持有旧地址 0xA000
let r2 = &mut s;  // r2 也持有旧地址 0xA000
r1.push_str("!"); // r1 触发扩容,s 搬到 0xB000,r2 的地址立即失效
r2.push_str("?"); // ❌ r2 仍指向 0xA000,野指针访问

即使不发生搬家,两个入口同时写入同一块内存也会导致数据竞争(Data Race),产生不可预测的逻辑错误。

结论: 同一时间只允许一个"修改入口",保证修改操作对内存状态的完全控制。

编译器的执行流程

当你借出一个 &mut 时,编译器在背后执行以下逻辑:

  1. 锁定: 原变量(栈上的指针、长度、容量)进入"被独占借用"状态,禁止其他引用访问
  2. 独占修改: 可变引用是唯一合法的访问入口,修改完成后直接回写原变量的栈上字段
  3. 解锁: 可变引用的生命周期结束后,原变量重新解锁,外界拿到的永远是最新的有效地址

这整套流程全部在编译阶段完成,零运行时开销

总结

角度核心机制
所有者类型变量、数据结构字段、函数参数均可持有所有权
底层实现编译器维护静态账本,Move = 浅拷贝 + 账本转让
借用互斥根因是防止内存重分配后的野指针,而非抽象规则

本质认识: 所有权和借用规则是编译器的"静态逻辑检查",与 TypeScript 静态类型检查在理念上类似——用编译时的约束换取运行时的安全,且没有额外的性能代价。

基于 MIT 协议发布