一个所有者,与作用域尽头的划销
上一章我们确立了「一个值只有一个主人」。这一章讲故事的结尾:主人离开舞台(作用域)时,那本书会自动、准时被还回去 —— 没有 GC 来扫,也不用你手动 free。这件事有个名字叫 RAII,它是 Rust 敢说「零成本」的另一半地基。
值有生,就有死
所有权规则的完整版,其实是三句话:
一、每个值都有一个变量作为它的所有者。
二、任一时刻,只能有一个所有者。
三、所有者离开作用域时,值被自动释放(drop)。
前两条上一章讲透了(移动就是在维护「只能有一个」)。这一章是第三条。它简单得几乎不像个规则,但威力惊人 —— 因为它把「什么时候释放内存」这个在别的语言里要么靠 GC 猜、要么靠你手动记的难题,变成了一件由作用域的花括号自动决定的事。
fn main() {
let s = String::from("借来的书");
println!("{}", s);
} // ← s 在这个 } 处离开作用域,Rust 自动调用它的析构,
// 堆上那串字符在这一刻被释放。你什么都不用写。
没有 free(s),没有 s = null,没有等 GC 哪天心血来潮来收。花括号一闭,s 的资源当场归还。
划销的顺序:后借的先还
当多个值在同一个作用域里,它们的释放顺序是声明顺序的逆序 —— 后声明的先 drop。这符合栈的直觉,也符合「借书」的直觉:最后借的那本,最先还回去。
下面这台登记簿会算出每个值被 drop 的顺序。试试「嵌套作用域」那个例子 —— 内层作用域一闭合,里面的值立刻被还掉,比外层的早得多。这套顺序是真算出来的。
「嵌套作用域」那个例子值得盯一会儿:内层的 b 在内层 } 处就被释放了,而外层的 a 一直活到程序最后。作用域的嵌套,直接决定了资源的生命长短 —— 想让一个大对象早点释放?把它包进一个更小的 { } 里就行。这种对「何时释放」的精确掌控,是 GC 语言给不了的。
这套机制的名字:RAII
「资源的获取,绑定到一个对象的生命周期;对象一死,资源自动归还」—— 这个思想叫 RAII(Resource Acquisition Is Initialization,Rust 从 C++ 继承来的)。关键词是「资源」,而资源远不止内存:
- 一个文件句柄:
File离开作用域,文件自动关闭。你永远不会忘记close()。 - 一把锁:
MutexGuard离开作用域,锁自动释放。你永远不会忘记unlock(),也不会因为中途 return / panic 而漏掉解锁。 - 一个网络连接、一段数据库事务、一块 GPU 显存…… 全都可以「离开作用域即归还」。
这才是 drop 的真正分量:它把「用完要记得还」这件全世界程序员都会忘的事,变成了语言层面不可能忘的事。忘记关文件、忘记解锁、忘记释放连接 —— 这些在别的语言里靠 finally、靠 defer、靠 with、靠自觉去防的泄漏,在 Rust 里由作用域的花括号兜底。
自定义 drop:给「还书」加动作
你可以为自己的类型实现 Drop,规定它被释放时该做什么。这里先只给你看个样子,第 16 章会正式讲:
struct Guard { name: String }
impl Drop for Guard {
fn drop(&mut self) {
println!("{} 被归还了", self.name); // 离开作用域时自动跑
}
}
fn main() {
let _a = Guard { name: String::from("甲") };
let _b = Guard { name: String::from("乙") };
} // 输出: 乙 被归还了 (先)
// 甲 被归还了 (后)—— 逆序,眼见为实
注意你不能手动调用 x.drop()(编译器会拦你,否则就会 double drop)。想提前释放,用标准库的 drop(x) 函数把所有权交给它 —— 它拿走、然后立刻让值离开作用域。这正是上一节 demo 里「提前归还」那个例子干的事。
确定性,是它比 GC 强的地方
很多人以为「没有 GC」是 Rust 的缺点(要自己管内存多累啊)。恰恰相反 —— drop 带来的「确定性释放」,是它比 GC 更强、而不是更弱的地方。
| GC(Java / Go / Python) | drop / RAII(Rust) | |
|---|---|---|
| 什么时候释放 | 不确定,GC 说了算 | 确定,作用域一闭就释放 |
| 运行时开销 | 有:后台线程、停顿、内存冗余 | 零:就是几条 free 指令,编译期插好 |
| 非内存资源(文件/锁) | GC 不管,得靠 finally/defer/with | 同样自动归还,一视同仁 |
| 你要操心的 | 内存不用管,但资源泄漏要防 | 所有权理顺后,都不用管 |
GC 只解决了「内存」这一种资源,而且用「不确定何时释放」+「运行时开销」换来的。RAII 把所有资源都纳入同一套「作用域结束即归还」的规则,还不花一分钱运行时。代价只有一个:你得先把所有权理清楚 —— 而这,正是这一卷在帮你做的事。
上一章说函数调用会移动所有权。那被移进去的值在哪 drop?答案顺理成章:在函数结束时 drop(除非又被返回移出去)。所有权在哪,drop 的责任就在哪。所以你 consume(book) 之后 book 用不了了 —— 不只是「所有权走了」,而是那本书很可能已经在 consume 结束时被还掉了。所有权、移动、drop,是同一件事的三个视角。
回流
Java / Kotlin:你写过多少 try (...) finally { close() }?Kotlin 的 use { }、Java 的 try-with-resources,本质就是「作用域结束,自动清理」—— 但它们是你要显式套上去的语法糖,而且只在你记得用的时候有效。忘了套,就泄漏。Rust 的 drop 是默认发生、无处不在的,不需要你想起来。而 finalize() 那条路(靠 GC 来跑清理)早已被官方弃用,因为它「什么时候跑、跑不跑」全不确定 —— 那正是 GC 管资源的死穴。
Go:defer file.Close() 很好用,也是同一个思想。但它是函数级的、要你手写、而且 defer 攒多了有开销;Rust 的 drop 是作用域级的、自动的、零开销的。
C++:RAII 的老家。Rust 继承了它的全部好处,还补上了它缺的那块 —— 因为有所有权和借用检查,Rust 里不会出现 C++ 那种「drop 之后又被用到」(use-after-free)。同样的 RAII,Rust 版是安全的。
这一章的一句话
值离开作用域就自动 drop,逆序归还,不只是内存,文件和锁都一视同仁 —— 这就是 RAII:把「用完要还」从一件会忘的事,变成一件不可能忘的事,而且不花一分钱运行时。
到这里,「一个所有者」的完整生命 —— 生(移动进来)、住(拥有)、死(drop 归还)—— 你都见过了。但一直「独占」太不方便:我只想读一下,难道每次都要把所有权交出去、再倒手要回来?下一章,登场的是让这一切变优雅的东西 —— 借用。