先看一段「诡异」的代码
let s1 = String::from("你好");
let s2 = s1;
println!("{}", s1); // 编译错误!
来自任何其他语言的程序员看到第三行报错都会愣一下:我就赋了个值而已,怎么 s1 就不能用了?这一章我们就把这份「诡异」拆解开。它是 Rust 所有安全保证的地基,也是这门语言和你之前学过的每一门语言真正不同的地方。
先补一分钟硬件常识:栈与堆
程序用的内存分两块。栈(stack)像一摞盘子:大小固定、按顺序放取、极快,装着大小在编译期就确定的小数据(整数、布尔值)。堆(heap)像一个大仓库:想放多大的东西都行,但要先「申请」一块、拿到它的地址,用完了得有人「归还」。字符串的内容就存在堆里——因为它可长可短,栈上只放一张写着仓库地址和长度的「提货单」。
第一章说的那些内存事故,几乎全出在堆上:忘了归还(泄漏)、还了两次(重复释放)、还了还在用(悬垂指针)。问题归结为一句话:堆上的每块内存,到底谁负责归还?
所有权三规则
Rust 的回答,精确到只有三句话:
- 每个值都有一个变量,是它的所有者(owner,主人);
- 任一时刻,一个值只能有一个主人;
- 主人离开自己的作用域(scope,就是它所在的那对花括号),这个值立刻被销毁、内存立刻归还。
第三条就是 Rust 不要垃圾回收器的秘密:内存归还是编译期就排定的——编译器在主人离开作用域的那一行,自动插入了归还内存的代码(这个机制叫 Drop,大白话是「离场自动清扫」)。不用你手动 free,所以没有「忘了还」和「还两次」;不用运行时管家盯着,所以没有停顿。确定性极强:哪一行还内存,你看着代码就能指出来。
回到那段诡异代码:移动,不是复制
现在能解释开头了。let s2 = s1; 发生了什么?栈上那张「提货单」(地址+长度)被复制了一份给 s2,但仓库里的货物只有一份。如果两张提货单都有效,两个主人都离场时就会归还同一批货物两次——重复释放,正是第一章说的事故。所以 Rust 的做法是:赋值即过户。s2 = s1 之后,s1 当场作废,主人换成了 s2。这个操作叫移动(move)。
类比:房子过户。把房产证交给新房主的那一刻,旧房主的证就失效了——世上绝不能同时存在两张指向同一套房子的有效房证,否则两家人都以为房子是自己的,拆迁款就得赔两次。Rust 的编译器就是那个绝不允许一房两证的登记处。
函数传参也一样过户:
fn main() {
let s = String::from("你好");
greet(s); // s 的所有权过户进了函数
// println!("{}", s); // 这里再用 s 就报错了
}
fn greet(msg: String) {
println!("{}", msg);
} // msg 在这里离场,内存自动归还
如果你真的想要两份独立的货物,就明说:let s2 = s1.clone();。clone 会把堆里的内容完整复制一份,两张提货单指向两批货,互不干扰。代价是真花了复制的开销——又一次,Rust 的风格:贵操作必须写在脸上,不许偷偷发生。
那为什么整数可以随便复制?
细心的你会发现:
let a = 5;
let b = a;
println!("{} {}", a, b); // 好好的,没报错
因为整数整个儿就在栈上,没有「提货单+仓库」两层结构,复制它就是全量复制,便宜且无歧义。这类类型实现了 Copy 标记——大白话是「我这类东西复制起来就是顺手一抄,不存在过户问题」。整数、浮点、布尔、字符都属于此类。凡是要碰堆的(String、Vec 等),都不可能是 Copy,一律走移动。
这一章你带走了什么
一个看似苛刻的规则——「一个值一个主人,赋值即过户」——直接把内存泄漏、重复释放这两大类事故从语言层面删掉了,而且不需要任何运行时开销。但你一定已经感到了不方便:难道每次把数据交给函数用一下,就得过户、从此别见了?当然不是。下一章的「借用」就是为这个不方便设计的——而它的规矩,同样由编译器强制执行。