本页目录
Rust I · 所有权与借用检查
对标:The Rust Programming Language(官方书)/ Stanford CS110L | 前置:csapp-04(内存之痛)、pl-02(三条内存管理路线) Rust 是过去十年最重要的系统语言创新——它做到了看似矛盾的事:没有垃圾回收、也不手动管理内存,却在编译期消灭了内存 bug 和数据竞争。秘密是所有权(ownership)——把"谁拥有内存、何时释放"编码进类型系统。这一页把 csapp-04 的所有内存灾难和 pl 线的类型纪律汇到一起:你会看到 C 的痛如何被编译器一条条提前拦下。本站系统线与语言线在此交汇。
学习层:把一个值交给别人后,原变量为什么不能继续用?
具体谜题:move、shared borrow 与 mutable borrow
有 let s=String::from("hi"); let t=s; println!("{}", s);。如果允许最后一次读取,谁会在析构时释放同一个 buffer?改成 let t=&s,再在借用期间执行 s.push('!'),又是哪条不变量被破坏?
先预测编译器的三种回应
预测:① move 后 s 被标记为不可用,避免 double free/use-after-move;② 共享借用期间允许多个读者但不允许写者;③ 可变借用期间必须独占,借用结束后所有权才恢复可用。
最小心智模型:资源有唯一主人,引用有时间区间
Rust 将堆资源绑定到拥有它的值,作用域结束时自动 drop;借用是带生命周期的临时权限,不转移所有权。核心不是“禁止指针”,而是让别名、可变性和释放时机在编译期有可检查的账本。
形式机制与不变量
所有权不变量是每个资源恰有一个 owner,且 owner drop 后不存在可用引用。借用规则可写成:在任一生命周期区间内,要么存在任意多个 \(&T\),要么存在一个 \(&mut T\),二者不能同时存在;引用必须满足 \(\mathrm{lifetime}(&T)\subseteq\mathrm{lifetime}(T)\)。move 转移 owner,copy 类型按值复制,clone 则显式执行可能昂贵的复制。
编译器的 soundness 目标是:通过借用检查的 safe Rust 不会产生 dangling reference、double free 或 data race;这不是对 unsafe 块内部任意代码的自动证明。
反例与失效边界
- 借用检查器不保证逻辑正确、无死锁、无资源饥饿或所有 unsafe 抽象安全;它只证明其模型覆盖的性质。
- 内部可变性、引用计数、异步 self-reference 和 FFI 会引入额外约束;
Rc<RefCell<T>>的运行时借用检查不是编译期万能替代。 - 为了通过编译而滥用 clone 会隐藏所有权设计问题并产生真实内存/性能成本。
迁移任务:把 C 的内存图交给 borrow checker
从 csapp-04 选一个 use-after-free 或双重释放案例,先画 owner/borrow 时间线,再用 Rust 表达。把 L11 的真实所有权实验与本页模型对应,说明编译器拒绝了哪条边、运行时仍需测试什么,并连接 pl-02 的 GC 与 cpp-01 的 RAII。
无 JavaScript 时的静态读法:s=String("hi") 拥有一个 buffer;执行 t=s 后 owner 转移到 t,s 变为 moved,最后只能由 t drop 一次。若 t=&s,可存在多个只读借用;但在借用区间内执行 s.push('!') 需要可变独占,编译器拒绝。交互版切换 move、shared borrow、mutable borrow 和作用域,显示所有权图与生命周期区间。
\n+
| 操作 | owner | 活跃引用 | 结果 |
|---|---|---|---|
| 初始化 s | s | 无 | 通过 |
| move s→t | t | 无 | s 不可用 |
| borrow &s | s | 多个 \(&T\) | 只能读 |
| borrow &mut s | s | 一个 \(&mut T\) | 独占读写 |
\n+
\n+\n+## 1. 问题回顾:内存管理的三难
pl-02 讲过三条路线,各有硬伤:
- 手动(C):快、可控,但泄漏 / 悬垂指针 / use-after-free / 二次释放 / 数据竞争——业界 ~70% 安全漏洞的来源(csapp-04)。
- GC(Java/Python):安全省心,但运行时开销 + 停顿——不适合系统级/实时。
- Rust 的第三条路——所有权:编译期确定每块内存何时释放,既无 GC 开销、又无内存 bug。代价是要接受借用检查器的"管教"(学习曲线)。
Rust 的赌注:把内存安全从"运行时检查/程序员自律"提前到"编译期类型检查"——编译通过就保证无这类 bug。这是 pl 线"把运行时错误编码进类型"哲学的巅峰应用。
2. 所有权三定律
Rust 的核心,就三条规则:
- 每个值有唯一的所有者(owner)(一个变量)。
- 同一时刻只有一个所有者。
- 所有者离开作用域时,值被自动释放(drop)。
规则 3 = 自动的、确定性的内存释放——不用手动 free(无泄漏),也不用 GC(无停顿、无不确定回收时机)。"作用域结束即释放"是 C++ 的 RAII 思想被 Rust 变成语言强制规则(🔗 与 pl-02 GC 对比:GC 是运行时追踪可达性,所有权是编译期跟踪作用域)。
移动语义(move):let b = a; 若 a 是拥有堆内存的类型(如 String),所有权从 a 移动到 b,a 之后不可用(编译器拒绝再用 a)。这防止了"两个变量都以为自己拥有、各自释放 → 二次释放"(csapp-04 的灾难之一)——编译期就杜绝。要真复制用 .clone()(显式、你知道有成本)。
3. 借用与借用检查器:核心魔法
老是移动所有权太麻烦(传给函数就不能再用了)。借用(borrow)——临时引用而不夺走所有权,分两种,规则是 Rust 的灵魂:
借用铁律:任一时刻,要么多个不可变借用(
&T,只读),要么恰好一个可变借用(&mut T,可写)——二者不可同时。
这条"共享不可变、可变不共享(shared XOR mutable)"的规则,一举消灭多类 bug:
- 数据竞争(os-02/par 线的噩梦):数据竞争 = 两个线程同时访问、至少一个写、无同步。借用铁律在编译期就禁止"可变 + 共享"同时存在 → 数据竞争在编译期不可能(rust-02 展开)。"无畏并发"的字面兑现。
- 迭代器失效:遍历一个集合时改它(
&借用存在时不能&mut)——C++/Java 的运行时崩溃/异常,Rust 编译期拒绝。 - 悬垂引用:借用不能比被借用的数据活得久——生命周期(lifetime)检查保证"引用永远指向有效数据",use-after-free 编译期消灭。
借用检查器(borrow checker)就是执行这些规则的编译器组件。它拒绝你的代码时,几乎总是它对——它发现了一个你没注意的别名/生命周期问题。学 Rust 的过程就是学会"把借用检查器的拒绝读成设计信号"([实验 L11] 就是练这个)。
4. 生命周期:引用有效性的显式化
生命周期标注('a)告诉编译器"这些引用要活多久、谁不能比谁先死"。多数情况编译器自动推断(生命周期省略),复杂时要手写 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str——"返回的引用活得和输入一样久"。生命周期不是运行时的东西,是编译期的"引用有效性证明"(🔗 pl-01 类型即证明:生命周期是"这个引用不会悬垂"的证明)。初学最烧脑,但理解后你会发现它只是把"这块内存什么时候还有效"这个 C 程序员一直在脑子里心算的东西,变成编译器帮你验证。
5. 所有权带来的其他好处
Option<T>/Result<T,E>(pl-02 的 ADT):Rust 没有 null——"可能没值"用Option、"可能出错"用Result,编译器强制处理(match或?)。空指针解引用在 Rust 里不存在。- 无异常、错误即值:
Result让错误处理显式、可组合(?运算符优雅传播)——你总能看到一个函数会不会失败。 - 零成本抽象:迭代器、泛型、trait 等高层抽象编译后和手写底层代码一样快(pl 线的抽象 + 系统线的性能兼得)——这是 Rust 相比 C++ 的设计承诺。
读法:Rust 的学习曲线陡,是因为它把"内存和并发的正确性"这个 C 程序员用多年经验和纪律维持的东西,前置成了必须一开始就交代清楚的编译期契约。痛在前面、收益在后面(编译过了就很少运行时内存/并发 bug)。对你这个"想补系统肌肉记忆"的人,Rust 是最好的老师——它逼你把 csapp-04 那些隐性的内存规则显式想清楚。
6. 练习与要点
例 1(移动 vs 借用) 写一个函数接收 String——用 String(移动,之后原变量不可用)vs &String(借用,之后还能用),体会所有权转移。理解"传值即交出所有权"。
例 2(借用检查器拒绝你) 故意写"持有不可变借用时又要可变借用"的代码,读编译器错误、理解它拦下了什么潜在 bug——[L11] 的核心练习:把拒绝读成保护。
例 3(生命周期必要性) 写一个返回两个字符串引用中较长者的函数,看编译器要求生命周期标注——理解"编译器需要知道返回的引用来自哪个输入、能活多久"。生命周期从抽象变具体。\(\blacksquare\)
▶ 实验 L11(Rust 所有权谜题集):
labs/L11-rust-ownership/—— 一组"借用检查器拒绝"的代码,逐个理解为什么被拒 + 如何修。跑在 Mac(Rust)。目标:把编译错误读成设计信号,而非障碍。
下一页:Rust II——并发、trait 与 unsafe 边界:所有权如何兑现"无畏并发",trait 的抽象能力,以及什么时候必须跳出安全网。