Rust I · 所有权与借用检查
对标:The Rust Programming Language(官方书)/ Stanford CS110L | 前置:csapp-04(内存之痛)、pl-02(三条内存管理路线) Rust 是过去十年最重要的系统语言创新——它做到了看似矛盾的事:没有垃圾回收、也不手动管理内存,却在编译期消灭了内存 bug 和数据竞争。秘密是所有权(ownership)——把"谁拥有内存、何时释放"编码进类型系统。这一页把 csapp-04 的所有内存灾难和 pl 线的类型纪律汇到一起:你会看到 C 的痛如何被编译器一条条提前拦下。本站系统线与语言线在此交汇。
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 的抽象能力,以及什么时候必须跳出安全网。