所有权:编译器为什么拒绝我
借用:& 与 &mut 的两条规则
它在解决什么
移动那篇结束在一个死胡同:把值交给函数就是交出所有权,想继续用就得让函数还回来, 而那意味着每个只想读一下的函数都要返回一个元组。写第二个这样的函数你就会放弃。
借用补的就是这一块:在不转移所有权的前提下,让别人用一下。
同一个函数调用两次也没问题,签名也恢复了正常。 它的对照项就是移动那篇里那个拿所有权的版本 —— 第二次调用就编译不过了。
fn len_of(s: &String) -> usize { // 借
s.len()
}
len_of(&s); // 借给它
&s 读作「s 的引用」。它不是指针(虽然运行时确实就是一个地址),
区别在于引用有期限,而期限由编译器盯着。这一篇讲的就是那个期限的规则。
两条规则
借用规则只有两条,而且第二条才是有争议的那条:
- 想要多少不可变借用(
&T)都行。 - 可变借用(
&mut T)同时只能有一个,而且它存在期间不能有任何不可变借用。
换一句话说:要么多人读,要么一人写,不能同时。
第一条没什么可说的,三个不可变借用同时存在完全正常。
第二条会拦下两种写法。两个可变借用报 E0499:
error[E0499]: cannot borrow `s` as mutable more than once at a time
一个可变加一个不可变报 E0502:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
两个码值得分清:E0499 是「可变撞可变」,E0502 是「可变撞不可变」。
看到码就知道该去找哪一个借用。
这条规则在换什么
「同时只能有一个写者」听起来像是为了并发。但这段代码是单线程的, 编译器照样拦 —— 因为它防的根本不是并发,是别名 + 可变这个组合本身。
最直观的例子是边遍历边往里加:
let mut v = vec![1, 2, 3];
for x in &v {
if *x == 2 {
v.push(4); // ← E0502
}
}
你已经会的那门语言怎么处理这件事。下面这张表是真跑出来的,
不是印象 —— 同一段逻辑(遍历 [1,2,3],遇到 2 就往里加个 4)在各语言里跑一遍:
| 语言 | 实际发生了什么 |
|---|---|
| Java 17 | 运行时抛 ConcurrentModificationException |
| C# / .NET 10 | 运行时抛 InvalidOperationException |
| Go 1.27 | 不报错也不崩:循环按开始时的长度跑完 3 轮,append 进去的元素这一轮看不到,结束后 v = [1 2 3 4] |
C++(Apple clang 21,-O0) |
迭代器失效,未定义行为。我跑过两次,两次结果不一样(见下) |
| Rust | 编译不过 |
⚠️ Go 那一行值得单独说:它既不抛异常也不崩,看起来什么事都没有。
range 在循环开始时就把切片的长度定下来了,所以新加的元素这一轮压根不会被遍历到。
这不是 bug,但如果你以为循环会看到自己加进去的东西,那就是个悄无声息的逻辑错误 ——
比抛异常难查得多。
🚨 C++ 那一行值得单独展开,因为它比表格能说的更多。同一台机器、同一个编译器、 同一段代码,我前后跑过两次:
- 第一次:死循环。迭代器一直没等于
end(),我加了个计数器强行截断才停下来。 - 第二次:多跑了几轮之后正常结束,
size也对不上原本该有的值。
这不是我改了什么,是未定义行为本来就不承诺任何事 ——
push_back 扩容时原来那块内存被释放了,迭代器指向的地方还在不在、
里面是什么,全看分配器当时的心情。
⇒ 所以这一行在表里不作为断言(跨语言闸门里它是「只记录不断言」的那一档)。 一个「这次跑出来是 X」的结论对 UB 是没有意义的,而 Java 抛哪个异常、 Go 跑几轮,是规范定死的,那些才断言得起来。
而 Rust 拦它用的不是针对容器的特判,就是上面那两条:
for x in &v 借走了 v(不可变),v.push 要一个可变借用,两者撞上了。
同一条规则顺手挡掉了这一整类问题。
⇒ 这就是那条规则在换的东西:**你放弃了「一边读一边改」这种写法, 换来这一类 bug 在编译期就不存在。**解法通常也很朴素 —— 先把要加的攒起来,循环结束再合并。
借用的期限:到最后一次用为止
规则二读起来很狠,但真写起来没那么憋屈,因为借用的有效期不到作用域结尾。
这一条是本篇最该记住的:两段代码一个字符都不差, 只是换了顺序,一个编译不过,一个跑得通。
let a = &s;
s.push('!'); // ← E0502:a 后面还要用
println!("{a}");
// ---- 只是把两行换了个位置 ----
let a = &s;
println!("{a}"); // a 的最后一次使用
s.push('!'); // ← 过了:a 已经没人要了
编译器认的是「这个引用最后一次被用是在哪」,而不是「它在哪个 {} 里」。
这个机制叫非词法生命周期(NLL,non-lexical lifetimes)。
⚠️ 这条对新手有个实际影响:把变量声明挪得离使用处近一点,经常就编译过了。
遇到借用冲突时,先看两个借用的使用范围是不是真的重叠,而不是急着 clone。
借不走的:E0505
还有一个码会在这一章反复出现。把一个正被借着的值搬走时:
error[E0505]: cannot move out of `s` because it is borrowed
它和移动那篇的 E0382 是一对,容易混:
E0382:已经搬走了,之后还在用它。E0505:想搬走,但搬走的那一刻还有人借着。
那条对照项两个码都报了,正好能对着看。
借用不能比被借的东西活得久
最后一条,它撞进了生命周期的地盘。想从函数里返回一个引用:
fn make() -> &String {
let s = String::from("hi");
&s // s 在函数结束时就被 drop 了
}
报的既不是 E0499 也不是 E0502,而是 E0106: missing lifetime specifier。
编译器在问一个前面几条都没问过的问题:这个引用要活多久?
而函数签名里没有任何信息能回答 —— 它不接受任何引用参数, 所以返回的引用不可能「跟着某个参数一起活」。这条路只能改成 把所有权交出去。
这个问题就是生命周期标注存在的理由:那些 <'a> 不是在延长什么,
它们是在陈述一件编译器自己推不出来的事。
下一步
这一篇留下的问题是:什么时候编译器能自己推出引用该活多久,什么时候要你写;
以及写出来的那个 <'a> 到底在说什么 ——
见《生命周期标注到底在说什么》。
全部篇目见 Rust 教程首页。
本篇示例
下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。
借一下就还:上一篇那个问题的正经解法
fn len_of(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
println!("{} {}", len_of(&s), len_of(&s));
println!("{s}");
}编译通过 · 输出 "5 5\nhello\n"
对照:把 `&String` 改回 `String`、调用处去掉两个 `&`
fn len_of(s: String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
println!("{} {}", len_of(s), len_of(s));
println!("{s}");
}编译不过:error[E0382]、error[E0382]
上一篇为了把所有权还回来,函数签名被迫返回一个元组。借用之后签名恢复正常,而且**调用多少次都行** —— 对照项里那个拿所有权的版本,第二次调用就编译不过了。
规则一:不可变借用要多少有多少
fn main() {
let s = String::from("hello");
let a = &s;
let b = &s;
println!("{a} {b} {s}");
}编译通过 · 输出 "hello hello hello\n"
对照:把 `let b = &s;` 改成 `let b = s;`(不是借,是搬走)
fn main() {
let s = String::from("hello");
let a = &s;
let b = s;
println!("{a} {b} {s}");
}编译不过:error[E0505]、error[E0382]
对照项一口气报了**两个**码,而且正好是一对:`E0505` 是「`s` 正被 `a` 借着,不能把它移走」,`E0382` 是「已经移走了,最后那个 `{s}` 还在用」。两者的区别值得记 —— **E0382 看的是「搬走之后」,E0505 看的是「搬走的那一刻还有没有人借着」**。
规则二:可变借用同时只能有一个
fn main() {
let mut s = String::from("hello");
let a = &mut s;
let b = &mut s;
println!("{a} {b}");
}编译不过 · error[E0499]
rustc 原文
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> borrow-mut-exclusive.rs:4:13
|
3 | let a = &mut s;
| ------ first mutable borrow occurs here
4 | let b = &mut s;
| ^^^^^^ second mutable borrow occurs here
5 | println!("{a} {b}");
| - first borrow later used here对照:只留一个可变借用
fn main() {
let mut s = String::from("hello");
let a = &mut s;
a.push('!');
println!("{a}");
}编译通过,输出:"hello!\n"
规则二的另一半:可变借用不能和不可变借用共存
fn main() {
let mut s = String::from("hello");
let a = &s;
s.push('!');
println!("{a}");
}编译不过 · error[E0502]
rustc 原文
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> borrow-mixed.rs:4:5
|
3 | let a = &s;
| -- immutable borrow occurs here
4 | s.push('!');
| ^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{a}");
| - immutable borrow later used here对照:把 `println!("{a}")` 挪到 `s.push` **之前**(一行代码都没改,只换了顺序)
fn main() {
let mut s = String::from("hello");
let a = &s;
println!("{a}");
s.push('!');
}编译通过,输出:"hello\n"
⭐ 这条对照项是本篇最该记住的一条:**两段代码一个字符都不差,只是换了顺序,一个编译不过一个跑得通**。借用的「有效期」不到作用域结尾,而是到它**最后一次被用**为止 —— 这叫非词法生命周期(NLL)。
真实形状:边遍历边往里加
fn main() {
let mut v = vec![1, 2, 3];
for x in &v {
if *x == 2 {
v.push(4);
}
}
println!("{v:?}");
}编译不过 · error[E0502]
rustc 原文
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> borrow-iterate-and-push.rs:5:13
|
3 | for x in &v {
| --
| |
| immutable borrow occurs here
| immutable borrow later used here
4 | if *x == 2 {
5 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here对照:先把要加的攒起来,循环结束之后再 `extend`
fn main() {
let mut v = vec![1, 2, 3];
let mut add = Vec::new();
for x in &v {
if *x == 2 {
add.push(4);
}
}
v.extend(add);
println!("{v:?}");
}编译通过,输出:"[1, 2, 3, 4]\n"
这段代码在 Java 里是运行时的 `ConcurrentModificationException`,在 C++ 里是迭代器失效(未定义行为,可能什么事都没有,也可能随机崩)。Rust 把同一件事挪到了编译期 —— 而它用的不是什么特判,就是上面那两条借用规则:`for x in &v` 借走了 `v`,`v.push` 要可变借用。
借用不能比被借的东西活得久
fn make() -> &String {
let s = String::from("hi");
&s
}
fn main() {
println!("{}", make());
}编译不过 · error[E0106]
rustc 原文
error[E0106]: missing lifetime specifier
--> borrow-return-local-ref.rs:1:14
|
1 | fn make() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
1 | fn make() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
1 - fn make() -> &String {
1 + fn make() -> String {
|对照:返回 `String` 而不是 `&String`(把所有权交出去)
fn make() -> String {
let s = String::from("hi");
s
}
fn main() {
println!("{}", make());
}编译通过,输出:"hi\n"
这条的报错和前面几条都不一样,它问的是「这个引用要活多久」—— 而函数签名里没有任何信息能回答。这就是生命周期标注要解决的问题。