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 here4 | v.push(4); // ❌ changes the Vec while `first` is alive | ^^^^^^^^^ mutable borrow occurs here5 | 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)。
这个错误是什么意思
Section titled “这个错误是什么意思”编译器指出了三行代码,三行合起来看才能理解这个错误:
- 第 3 行:
&v[0]创建了一个指向v内部的只读引用。这就是不可变借用。 - 第 4 行:
v.push(4)需要修改v,所以它要拿一个可变借用。 - 第 5 行:又用到了
first。正是这一行,让第一个借用跨过第 4 行一直有效。
如果没有第 5 行,代码就能编译通过。问题不在于两个借用同时存在,而在于它们重叠了。
Rust 为什么要这样做
Section titled “Rust 为什么要这样做”规则是:可以有很多个读者,或者只有一个写者,但两者不能同时存在。这条规则你在借用那一页已经学过了。
对 Vec 来说,这里有一个非常具体的原因。Vec 把它的元素放在一整块连续的内存里。当你 push 时如果这块内存已经满了,Vec 会把所有元素搬到一块更大的新内存里,并释放旧的那块。任何指向旧内存的引用(比如 first)这时就指向了已经释放的内存。在 C++ 里,这就是著名的“迭代器失效”(iterator invalidation)bug,它可能让程序崩溃,也可能悄悄读到一堆垃圾数据。
这就像你记下了朋友的地址,然后朋友搬进了更大的房子。你的纸条看起来没什么问题,可它指向的已经是一栋空楼了。Rust 干脆拒绝编译这段代码,不让你跑去那栋空楼。
修复 1:先读完,再写入
Section titled “修复 1:先读完,再写入”首选这个方法。从 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]修复 2:把值复制出来
Section titled “修复 2:把值复制出来”当你在修改之后还需要旧值时,选这个方法。拿一份副本,而不是一个引用。对于数字,去掉 & 就行;对于 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 会占用一点内存,但代码清楚,一看就知道是对的。这通常是笔划算的买卖。
修复 5:使用索引
Section titled “修复 5:使用索引”其他修复都不合适时,再把它当作最后的手段。索引只是一个数字,不是借用,所以它不会妨碍修改:
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
- E0502 — Rust error codes index(英文):这个错误的官方解释。
- 使用 Vector 储存列表 — Rust 程序设计语言:里面正好有这个
let first = &v[0]; v.push(..)的例子,并解释了它为什么会失败。 - 引用与借用 — Rust 程序设计语言:“多个读者或一个写者”规则的完整讲解。
- Non-lexical lifetimes — The Edition Guide(英文):为什么现在借用在最后一次使用时结束,而不是等到右花括号。
Vec— 标准库文档(英文):retain、iter_mut、extend,以及其他能安全修改 vector 的方法。