跳转到内容

Rust 错误 E0502:cannot borrow as mutable because it is also borrowed as immutable(可变与不可变借用冲突)

E0502 的意思是:代码的另一部分还在通过引用读取某个值,你就想去修改它。用 vector 时这个错误特别常见,修复方法通常是挪动一行代码,或者复制出一个值。本页先给出修复方法,再解释 Rust 为什么坚持这样做。这个系列的其他错误,请看 Rust 编译错误详解。

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &v[0]; // a reference into the Vec
| - immutable borrow occurs here
4 | v.push(4); // ❌ changes the Vec while `first` is alive
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("First: {first}"); // ...because `first` is used here
| ----- immutable borrow later used here

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

fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // a reference into the Vec
v.push(4); // ❌ changes the Vec while `first` is alive
println!("First: {first}"); // ...because `first` is used here
}

编译器说:v 已经被不可变地借用了,所以不能再把它作为可变借用(“cannot borrow v as mutable because it is also borrowed as immutable”)。三条标注分别指出了不可变借用发生的位置(immutable borrow occurs here)、可变借用发生的位置(mutable borrow occurs here),以及之后又用到不可变借用的位置(immutable borrow later used here)。

编译器指出了三行代码,三行合起来看才能理解这个错误:

  1. 第 3 行:&v[0] 创建了一个指向 v 内部的只读引用。这就是不可变借用。
  2. 第 4 行:v.push(4) 需要修改 v,所以它要拿一个可变借用。
  3. 第 5 行:又用到了 first。正是这一行,让第一个借用跨过第 4 行一直有效。

如果没有第 5 行,代码就能编译通过。问题不在于两个借用同时存在,而在于它们重叠了。

规则是:可以有很多个读者,或者只有一个写者,但两者不能同时存在。这条规则你在借用那一页已经学过了。

对 Vec 来说,这里有一个非常具体的原因。Vec 把它的元素放在一整块连续的内存里。当你 push 时如果这块内存已经满了,Vec 会把所有元素搬到一块更大的新内存里,并释放旧的那块。任何指向旧内存的引用(比如 first)这时就指向了已经释放的内存。在 C++ 里,这就是著名的“迭代器失效”(iterator invalidation)bug,它可能让程序崩溃,也可能悄悄读到一堆垃圾数据。

这就像你记下了朋友的地址,然后朋友搬进了更大的房子。你的纸条看起来没什么问题,可它指向的已经是一栋空楼了。Rust 干脆拒绝编译这段代码,不让你跑去那栋空楼。

首选这个方法。从 Rust 2018 开始,借用在它最后一次使用时就结束了,而不是等到右花括号。这叫做非词法作用域生命周期(non-lexical lifetimes,NLL)。所以,只要在修改 vector 之前用完那个引用就行:

fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("第一个:{first}"); // 最后一次使用 `first`:借用到这里结束
v.push(4); // ✅ 已经没人在读了
println!("现在:{:?}", v);
}

输出:

第一个:1
现在:[1, 2, 3, 4]
▶ 动手试一试

当你在修改之后还需要旧值时,选这个方法。拿一份副本,而不是一个引用。对于数字,去掉 & 就行;对于 String,用 .clone():

fn main() {
let mut queue = vec![String::from("deploy"), String::from("test")];
let next = queue[0].clone(); // 我们自己的副本,不是引用
queue.push(String::from("release"));
println!("下一个任务:{next}");
println!("队列:{:?}", queue);
}

输出:

下一个任务:deploy
队列:["deploy", "test", "release"]
▶ 动手试一试

如果是 Vec<i32>,写 let first = v[0]; 就能免费得到一份副本,因为 i32 是 Copy 的。

修复 3:用 iter_mut 或 retain 原地修改元素

Section titled “修复 3:用 iter_mut 或 retain 原地修改元素”

如果你写了 for p in prices.iter() 这样的循环,然后在循环里调用 prices.remove(i) 或修改元素,也会得到 E0502。标准库正好有专门处理这种情况的方法:

fn main() {
let mut prices = vec![120, 45, 300, 80];
// 原地修改每个元素
for p in prices.iter_mut() {
*p = *p * 9 / 10; // 打九折
}
// 删掉不想要的元素
prices.retain(|&p| p >= 50);
println!("{:?}", prices);
}

输出:

[108, 270, 72]
▶ 动手试一试

iter_mut 给每个元素一个 &mut,所以任何时候都只有一个写者。retain 会帮你安全地删除元素,一遍就完成。参见 vector和迭代器。

修复 4:先收集改动,再统一应用

Section titled “修复 4:先收集改动,再统一应用”

当你需要根据读到的内容添加元素时,选这个方法。先读取并收集,再在单独的一步里写入:

fn main() {
let mut scores = vec![10, 25, 7, 40];
let bonuses: Vec<i32> = scores.iter().filter(|&&s| s > 20).map(|s| s * 2).collect();
scores.extend(bonuses); // 读取的借用已经结束,可以写入了
println!("{:?}", scores);
}

输出:

[10, 25, 7, 40, 50, 80]
▶ 动手试一试

多出来的那个 Vec 会占用一点内存,但代码清楚,一看就知道是对的。这通常是笔划算的买卖。

其他修复都不合适时,再把它当作最后的手段。索引只是一个数字,不是借用,所以它不会妨碍修改:

fn main() {
let mut scores = vec![10, 25, 7];
for i in 0..scores.len() {
if scores[i] > 20 {
scores.push(scores[i] * 2);
}
}
println!("{:?}", scores);
}

输出:

[10, 25, 7, 50]
▶ 动手试一试

0..scores.len() 在循环开始之前就算好了,只算一次,所以新加的元素不会被遍历到。按索引删除元素时要小心:位置会移动,你可能会跳过某些元素,甚至触发 panic。要删除元素,retain(修复 3)更安全。

一边遍历 Vec,一边往里 push。 for s in &scores { scores.push(..) } 在整个循环期间都借用着 scores。修复:先收集,之后再 extend(修复 4),或者按索引循环(修复 5)。

遍历一个字段时调用 &mut self 方法。 这在 impl 块里非常常见:

fn audit(&mut self) {
for item in &self.items {
self.record(item); // ❌ cannot borrow `*self` as mutable because it is also borrowed as immutable
}
}

self.record 会可变地借用整个 self,其中也包括 self.items。修复:直接操作另一个字段(self.log.push(item.clone())),因为 Rust 允许同时借用两个不同的字段;或者先把元素克隆出来。参见结构体。

插入 HashMap 时还拿着其中的一个值。 先 let apples = stock.get("apples").unwrap();,再 stock.insert(..),然后又用 apples,就会得到 E0502。修复:用 let apples = *stock.get("apples").unwrap(); 或 .copied() 把值复制出来。参见 HashMap。

  • E0499:同时有两个可变借用。道理相同,只不过是两个写者,而不是一个读者加一个写者。
  • E0382:你移动了一个值,然后又使用了它。用移动代替借用来修复 E0502 时,经常会冒出这个错误。
  • E0106:返回引用的函数缺少生命周期标注。
  • 所有编译错误:错误页面的完整列表。

小测验

1. 为什么 let first = &v[0]; v.push(4); println!("{first}"); 编译不过?

2. 你想从一个 Vec<i32> 里删掉所有小于 50 的元素。修复 E0502 最干净的写法是什么?

3. 在现代 Rust 里,不可变借用什么时候结束?

得分: 0 / 3