借用:不转移,只出借
上一章结尾我们卡在一个笨拙处:只想读一下值,却要把所有权整个交出去、再倒手要回来。借用就是为解决这个笨拙而生的 —— 它让你「看一眼」甚至「改一下」,而不夺走所有权。代价是接受一条铁律:要么很多人一起读,要么一个人独自写,二者不可兼得。这一章讲清这条铁律,为什么它天经地义。
先解决那个笨拙
回想上一章:想让函数读一下字符串又不夺走所有权,得让它「拿走再还回」,倒手一次。有了借用,就干净了 —— 参数写成 &String(或更常见的 &str),函数借着看,看完自动还,所有权始终在调用者手里:
fn length(s: &String) -> usize { // 借一下,不拿走
s.len()
} // 借用在这里结束,什么都不 drop
fn main() {
let book = String::from("借还");
let n = length(&book); // 把 &book 借出去
println!("{} 有 {} 字节", book, n); // ✓ book 还是我的,随便用
}
&book 读作「book 的引用」。它是一个指向 book 的指针,但带着一个编译期的承诺:我只是借看,不拥有,不负责释放。所以借用结束时什么都不会被 drop —— 你不能 drop 一本不属于你的书。
两种借法
借用分两种,区别只在「能不能改」:
- &T 共享借用:只读。你可以看,不能改。可以同时有很多个 —— 一堆人围着同一本书一起读,互不干扰。
- &mut T 可变借用:可读可写。但同一时刻只能有一个,而且它独占期间,连别的只读借用都不许有。要改,要拿
&mut;要拿&mut,就得让被借的值声明成mut。
fn append(s: &mut String) { // 借来改
s.push_str("(第 2 版)");
}
fn main() {
let mut book = String::from("借还"); // 要被可变借用,得是 mut
append(&mut book); // 借出可变引用
println!("{}", book); // 借还(第 2 版)
}
那条铁律,与它背后的道理
把两种借法的限制合起来,就是贯穿全书的那一句:
任一时刻,对同一个值,你只能拥有以下两者之一:
· 任意多个 &T 共享借用(大家一起读),
· 或者恰好一个 &mut T 可变借用(一个人独自写)。
不能同时有共享和可变,也不能同时有两个可变。读可以扎堆,写必须独处。
为什么?很多人以为这是 Rust 强加的洁癖,其实它在防一件非常具体的祸事:一边有人读、一边有人改。
设想一个经典场景:你正在遍历一个列表(这是「读」),循环体里往列表里 push 一个元素(这是「改」)。push 可能触发扩容 —— 底层缓冲区搬家到新地址,旧地址失效。而你手里那个遍历用的指针,还指着旧地址。下一步解引用,就是访问一块已经作废的内存。
let mut v = vec![1, 2, 3];
for x in &v { // 借出共享引用来遍历(读)
if *x == 2 {
v.push(4); // ✗ 想在读的同时改 —— 编译器当场拦下
}
}
这个 bug 有名字:迭代器失效。Java 里它是运行时的 ConcurrentModificationException,C++ 里它是不声不响的未定义行为(可能崩、可能读到垃圾、可能看起来正常然后某天崩)。而在 Rust 里,它连编译都过不了 —— 因为遍历借走了 v 的 &,此刻你不可能再拿到改它所需的 &mut。「共享 XOR 可变」就是把「读时不许改」这条安全律,钉进了类型系统。
更妙的是:这条律同时也是数据竞争的定义。多线程数据竞争的本质,就是「两个线程同时访问一块内存,至少一个在写,且没有同步」。而「至少一个在写」= 存在 &mut,「同时访问」= 别名。禁掉「共享与可变并存」,就从根上禁掉了数据竞争 —— 这就是为什么到了卷 V,Rust 敢叫「无畏并发」:并发安全不是另一套规则,就是这一条借用律的直接后果。先记住这个伏笔。
借用不能比主人活得久
借用还有第二条约束,同样天经地义:引用不能比它指向的值活得久。你不能借一本书,然后在书被销毁之后还拿着借条去看它 —— 那借条指向的是一块已经不存在的东西(悬垂引用)。
let r;
{
let x = String::from("临时的");
r = &x; // r 借了 x
} // ✗ x 在这里没了,可 r 还想指着它
println!("{}", r); // error[E0597]: `x` does not live long enough
这条约束,展开来就是让无数人头疼的生命周期。但你看,它本身一点都不神秘 —— 它只是「借条不能比书活得久」这句大白话。第 8 章我们会用一台专门的 demo,把它彻底画清楚。
方法的三种接收者
你会在方法定义里反复见到 self 的三种写法,现在正好一次看懂 —— 它们就是「移动 / 共享借 / 可变借」的方法版:
| 写法 | 含义 | 调用后原对象 |
|---|---|---|
&self | 共享借用自己(只读方法,最常见) | 还能用 |
&mut self | 可变借用自己(要改状态的方法) | 还能用 |
self | 拿走自己的所有权(消费型方法,如 into_xxx) | 用不了了 |
所以当你看到某个方法签名是 fn into_bytes(self),你立刻就知道:调它会消耗掉这个对象。所有权、借用的那套心智,在方法这里原样复用。
上面为了讲解写了 &String,但真实代码里你几乎总该写 &str。原因第 9 章讲 —— 简单说,&str 更通用(字符串字面量、String 都能传给它),限制更少。现在先有个印象:参数要「只读地看一段字符串」,默认写 &str。这是 Rust 里最常见的地道写法之一。
回流
Java / Kotlin / Python / JS:你传一个 ArrayList 给方法,方法拿到的是同一个列表的引用,它能随便改,改了你这边也变 —— 这就是「共享 + 可变」同时存在,Rust 眼里的头号危险,在这些语言里却是默认行为。所以这些语言里才有那么多「防御性拷贝」「不可变集合」「别把内部 list 直接返回出去」的最佳实践 —— 它们是在手动、靠自觉地维护 Rust 编译器免费强制的那条铁律。
C++:const T& 大致对应 &T,T& 对应 &mut T。但 C++ 不检查别名:你可以同时握着一个 const T& 和一个 T& 指向同一个对象,读到写到一半的状态,编译器不吭声。Rust 把「共享 XOR 可变」变成强制的,于是也顺手打开了别的优化空间 —— 编译器知道 &mut 是独占的,可以放心地做别名分析和优化(这正是 C 的 restrict 关键字苦苦想表达的东西)。
结论还是那句:Rust 没发明新麻烦,它只是把你本来就该守、却没人帮你守的规矩,变成了编译器守。
这一章的一句话
借用让你不夺走所有权就能读或改;代价是那条铁律 —— 读可以扎堆,写必须独处。它不是洁癖,它是「读时不许改」和「无数据竞争」的同一个源头。
规则讲清了。但规则要长在手上,得靠亲手撞。下一章 —— 本书的招牌 —— 是一台真的借用检查器,你改代码,它当场判,还告诉你为什么。是时候去和它过几招了。