跳转到内容

Rust 错误 E0499:cannot borrow as mutable more than once at a time(两个可变借用)

E0499 的意思是:你想同时拥有同一个值的两个可变引用(&mut)。这是借用检查器最常见的错误之一,修复起来通常不费事:很多时候只要挪动一行代码就行。(想找别的错误?请看编译错误总览。)

error[E0499]: cannot borrow `balance` as mutable more than once at a time
--> src/main.rs:5:20
|
4 | let deposit = &mut balance;
| ------------ first mutable borrow occurs here
5 | let withdraw = &mut balance;
| ^^^^^^^^^^^^ second mutable borrow occurs here
6 |
7 | *deposit += 50;
| -------------- first borrow later used here

产生这个错误的程序是这样的:

fn main() {
let mut balance = 100;
let deposit = &mut balance;
let withdraw = &mut balance;
*deposit += 50;
*withdraw -= 30;
println!("Balance: {balance}");
}

编译器说:同一时间不能把 balance 可变地借用多次(“cannot borrow balance as mutable more than once at a time”)。三条标注分别是:第一个可变借用发生的位置(first mutable borrow occurs here)、第二个可变借用发生的位置(second mutable borrow occurs here),以及之后又用到第一个借用的位置(first borrow later used here)。

编译器指出了三个地方,合起来就是整个故事:

  1. 第 4 行:deposit 成为 balance 的一个可变借用。
  2. 第 5 行:withdraw 想成为 balance 的第二个可变借用。
  3. 第 7 行:在第 5 行之后,deposit 还在被使用。所以第二个借用开始时,第一个借用还活着。

第三条标注最关键。两个 &mut 借用只有在生命周期重叠时才会出问题。如果 deposit 在第 5 行之前就用完了,就不会报错。

想象一份共享文档,只有一把“编辑”钥匙。谁拿着钥匙,谁就能修改文档。Rust 不会给钥匙配第二把:第二个人得等第一个人把钥匙还回来。

Rust 的借用规则是:任何时刻,你可以有很多个读者(&),或者一个写者(&mut),但写者绝不能超过一个。&mut 引用是一个承诺:“只要我还在,别人就不能碰这个值。”

这个承诺能防止真实的 bug。如果两段代码能同时修改同一个值,其中一段可能会让 Vec 扩容,而另一段还指向它的旧内存。在别的语言里,这会导致崩溃,或者数据被悄悄破坏。放到多个线程之间,这就是数据竞争(data race)。

有了这条规则,编译器就能确信:在 &mut 存在期间,它是访问这份数据的唯一途径。这份安全性没有任何运行时开销。代价是,有时你得更清楚地告诉编译器,你的两个借用其实并不冲突。

修复 1:先结束第一个借用,再开始第二个

Section titled “修复 1:先结束第一个借用,再开始第二个”

首选这个方法。 大多数 E0499 错误都能这样解决。借用一直持续到它最后一次使用,而不是到代码块结束。所以先用第一个引用把事情做完,再创建第二个:

fn main() {
let mut balance = 100;
let deposit = &mut balance;
*deposit += 50; // 最后一次使用 `deposit`:它的借用到这里结束
let withdraw = &mut balance; // ✅ 现在没有别人在借用了
*withdraw -= 30;
println!("余额:{balance}");
}

输出:

余额:120
▶ 动手试一试

很多时候你根本不需要这些额外的变量:先写 balance += 50; 再写 balance -= 30;,效果完全一样。

有时你确实需要同时拿着两个 &mut,但它们指向数据的不同部分。编译器不一定看得出来,所以你得明确告诉它。

对于 Vec 或 slice 里的两个元素,tasks[0] 和 tasks[1] 都会借用整个 tasks。用 split_at_mut(英文)把它切成两半,保证两半互不重叠:

fn main() {
let mut tasks = vec![String::from("write"), String::from("test")];
// 把 Vec 切成互不重叠的两半
let (left, right) = tasks.split_at_mut(1);
let first = &mut left[0];
let second = &mut right[0];
std::mem::swap(first, second);
println!("{tasks:?}");
}

输出:

["test", "write"]
▶ 动手试一试

(如果只是交换,tasks.swap(0, 1) 更简单。split_at_mut 适合真的需要用两个引用做事的场合。)

对于结构体,当你调用两个都接收 &mut self 的方法时(比如 p.name_mut() 和 p.score_mut()),也会出现同样的错误。每次调用都会借用整个结构体。如果改成直接借用字段,编译器就能看出它们是分开的:

struct Player {
name: String,
score: u32,
}
fn main() {
let mut p = Player { name: String::from("Ferris"), score: 0 };
// 直接借用字段,而不是通过方法
let name = &mut p.name;
let score = &mut p.score;
name.push_str(" 小螃蟹");
*score += 10;
println!("{} 有 {} 分", p.name, p.score);
}

输出:

Ferris 小螃蟹 有 10 分
▶ 动手试一试

修复 3:用索引,而不是一直拿着引用

Section titled “修复 3:用索引,而不是一直拿着引用”

一个经典场景:你用 iter_mut() 遍历一个 Vec,又想在循环里往它里面 push。迭代器在整个循环期间都拿着一个 &mut,所以 push 就成了第二个:

fn main() {
let mut queue = vec![1, 2, 3];
for job in queue.iter_mut() {
*job *= 10;
queue.push(*job); // ❌ E0499: `queue` is already borrowed by iter_mut()
}
}

当循环必须修改集合本身时,就改为遍历位置。索引只是一个数字,它什么都不借用:

fn main() {
let mut queue = vec![1, 2, 3];
// 遍历位置,而不是拿着一个指向 `queue` 内部的引用
for i in 0..queue.len() {
queue[i] *= 10;
let job = queue[i];
queue.push(job);
}
println!("{queue:?}");
}

输出:

[10, 20, 30, 10, 20, 30]
▶ 动手试一试

0..queue.len() 在循环开始之前就算好了,只算一次,所以新加的元素不会再被遍历。每个 queue[i] 借用只持续一行。

RefCell 把借用检查从编译时挪到了运行时。即使变量不是 mut,你也可以调用 borrow_mut(),只要同一时刻没有两个可变借用同时活着:

use std::cell::RefCell;
fn main() {
let balance = RefCell::new(100); // 注意:不需要 `mut`
*balance.borrow_mut() += 50; // 借用、修改,借用在 `;` 处结束
*balance.borrow_mut() -= 30;
println!("余额:{}", balance.borrow());
}

输出:

余额:120
▶ 动手试一试

说句实在话:RefCell 并没有取消这条规则,只是把检查推迟了。如果你让两个 borrow_mut() 守卫同时活着,程序能编译通过,但运行时会 panic:

use std::cell::RefCell;
fn main() {
let balance = RefCell::new(100);
let mut deposit = balance.borrow_mut();
let mut withdraw = balance.borrow_mut(); // 💥 `deposit` 还活着
*deposit += 50;
*withdraw -= 30;
}
thread 'main' (12345) panicked at src/main.rs:7:32:
RefCell already borrowed

panic 信息的意思是:RefCell 已经被借用了。所以,只有当数据确实是共享的(比如在 Rc<RefCell<T>> 里),而且修复 1–3 都不合适时,才去用 RefCell。它不是让编译器闭嘴的办法。

  • 在循环里存放 &mut。 在 for 循环里写 handles.push(&mut counter) 会报错,提示“mutably borrowed here in the previous iteration of the loop”(在上一轮循环里已经被可变借用了)。循环每转一圈,就给同一个值多加一个 &mut。修复: 存放索引或拥有所有权的值,或者直接在循环里修改这个值。
  • 对 HashMap 调用两次 get_mut。 先 let a = stock.get_mut("apples"),再 let p = stock.get_mut("pears"),就借用了 stock 两次。修复: 在再次调用 get_mut 之前先用完 a,或者用 stock.get_disjoint_mut(["apples", "pears"]) 一次拿到两个。
  • 两个闭包修改同一个变量。 let mut inc = || count += 1; 和 let mut inc2 = || count += 2; 在各自存活期间都拿着一个 &mut count。修复: 只用一个闭包,或者在创建第二个闭包之前最后一次调用第一个。参见闭包。

小测验

1. Rust 什么时候会报 E0499?

2. 你先创建 let a = &mut v;,最后一次使用 a,然后创建 let b = &mut v;。会发生什么?

3. 哪个工具能让你拿到同一个 slice 中不同元素的两个 &mut 引用?

得分: 0 / 3