所有权:编译器为什么拒绝我

借用:& 与 &mut 的两条规则

它在解决什么

移动那篇结束在一个死胡同:把值交给函数就是交出所有权,想继续用就得让函数还回来, 而那意味着每个只想读一下的函数都要返回一个元组。写第二个这样的函数你就会放弃。

借用补的就是这一块:在不转移所有权的前提下,让别人用一下。

同一个函数调用两次也没问题,签名也恢复了正常。 它的对照项就是移动那篇里那个拿所有权的版本 —— 第二次调用就编译不过了。

fn len_of(s: &String) -> usize {   // 借
    s.len()
}
len_of(&s);                        // 借给它

&s 读作「s 的引用」。它不是指针(虽然运行时确实就是一个地址), 区别在于引用有期限,而期限由编译器盯着。这一篇讲的就是那个期限的规则。

两条规则

借用规则只有两条,而且第二条才是有争议的那条:

  1. 想要多少不可变借用(&T)都行。
  2. 可变借用(&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"

这条的报错和前面几条都不一样,它问的是「这个引用要活多久」—— 而函数签名里没有任何信息能回答。这就是生命周期标注要解决的问题。