借用检查器常见错误清单
用途
这是一篇排错手册:编译报错时按错误码查。每个错误都给出「症状 → 原因 → 修法」,修法按推荐优先顺序排列。
核心心智三问,任何借用问题都先问这三句:
- 谁拥有这个值?
- 现在有谁在借它?(不可变还是可变)
- 这个借用要活到什么时候?(最后一次使用点在哪)
0. 修法的优先顺序
不要第一反应就
clone()按这个顺序考虑,前一步能解决就不要走到下一步:
- 改设计:把所有权转移到需要的地方,或让函数返回拥有型值
- 缩短作用域:把借用限制在更小的
{}里,或调整语句顺序(NLL 会帮忙)- 改成借用:
&T/&mut T/&str/&[T]- 用标准库工具:
split_at_mut、mem::take、mem::replace、entryAPI、retain、drainclone():确实是需要独立副本时Rc<RefCell<T>>/Arc<Mutex<T>>:真的需要共享可变状态时(代价是把编译期检查换成运行时风险)
1. E0382 —— 使用了已移动的值
error[E0382]: borrow of moved value: `s`let s = String::from("hi");
let t = s;
println!("{s}"); // ❌原因:String 不实现 Copy,赋值即移动所有权。
修法:
// ① 传引用,不转移所有权(最常用)
let t = &s;
println!("{s} {t}");
// ② 借用给函数
fn take(s: &str) { }
take(&s);
// ③ 提前复制(确实需要两份独立数据时)
let t = s.clone();
// ④ 调整顺序:先用完再移动
println!("{s}");
let t = s;复合场景:结构体字段被移动后,整个结构体不能再整体使用(部分移动)。
struct P { name: String, age: u32 }
let p = P { name: "a".into(), age: 1 };
let name = p.name; // name 被移动
// println!("{:?}", p); // ❌ 部分移动
println!("{}", p.age); // ✅ 未被移动的 Copy 字段仍可用修法:先借用(let name = &p.name;),或用 .. 解构后重建。
2. E0502 —— 可变借用与不可变借用重叠
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutablelet mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // ❌
println!("{first}");原因:first 还活着,push 可能重新分配内存使 first 悬垂。
修法:
// ① 先取值再修改(Copy 类型直接拷贝出来)
let first = v[0];
v.push(4);
println!("{first}");
// ② 用作用域隔开
{
let first = &v[0];
println!("{first}");
}
v.push(4);
// ③ 调整语句顺序(NLL:借用到最后一次使用为止)
let first = &v[0];
println!("{first}");
v.push(4); // ✅ 上面已经不用 first 了3. E0499 —— 同时存在两个可变借用
let mut s = String::from("a");
let r1 = &mut s;
let r2 = &mut s; // ❌修法:用作用域分隔;或用 split_at_mut 同时操作两个不相邻区域。
let mut data = vec![1, 2, 3, 4];
let (left, right) = data.split_at_mut(2);
left[0] = right[0]; // ✅ 同时持有两个可变借用,但区域不重叠多字段场景:借用检查器能识别不同字段是独立借用,但不能识别”通过方法访问的不同字段”。
struct S { a: Vec<i32>, b: Vec<i32> }
// ❌ 编译器认为 self 被整体借了两次
impl S {
fn bad(&mut self) {
// self.a.push(self.b.len());
}
}
// ✅ 先取出需要的值
impl S {
fn good(&mut self) {
let n = self.b.len();
self.a.push(n);
}
}4. E0505 / E0506 —— 借用期间移动 / 赋值
let mut s = String::from("a");
let r = &s;
let t = s; // ❌ E0505: cannot move out of `s` because it is borrowed
println!("{r}");let mut s = String::from("a");
let r = &s;
s = String::from("b"); // ❌ E0506: cannot assign to `s` because it is borrowed修法:先让借用结束(打印/用完),再移动或赋值。
5. E0507 —— 从借用里往外移
error[E0507]: cannot move out of `x` which is behind a shared referencefn take(v: &Vec<String>) -> String {
v[0] // ❌ 想把 String 移出借用
}修法:
fn take(v: &[String]) -> String {
v[0].clone() // ① 复制
}
fn take_owned(v: Vec<String>) -> String {
v.into_iter().next().unwrap() // ② 拿走所有权
}
// ③ 需要"取走但保留位置"时用 mem::take(要求 Default)
let mut s = String::from("a");
let taken = std::mem::take(&mut s); // s 变成空 Stringmem::replace 同理,用于”用新值换出旧值”。
6. E0596 —— 变量没声明为可变
let v = Vec::new();
// v.push(1); // ❌
let mut v = Vec::new();
v.push(1); // ✅注意:可变性属于绑定,不属于类型。let mut x: Foo 里的 mut 不会传染给字段的内部可变性——这与 RefCell 的内部可变性是两回事。
7. E0597 —— 活得不够久
let r;
{
let x = 5;
r = &x; // ❌ x does not live long enough
}
println!("{r}");修法:把被引用值提到外层作用域;或改成拥有型(String / Vec<T>)。
8. E0515 —— 返回局部变量的引用
fn bad() -> &String { // ❌
let s = String::from("x");
&s
}修法(按推荐顺序):
// ① 返回拥有型(最常见、最简单)
fn good() -> String { String::from("x") }
// ② 从入参借用返回(调用方保证数据活着)
fn good2(s: &str) -> &str { &s[..1] }
// ③ 真需要共享时用 Rc/Arc
fn good3() -> std::rc::Rc<String> {
std::rc::Rc::new(String::from("x"))
}9. E0716 —— 临时值在被借用期间就被丢弃
// ❌ 临时的 String 在本行结束即 drop
let s = String::from("hi").as_str();修法:先把所有者绑定住。
let tmp = String::from("hi");
let s = tmp.as_str();这个错误在链式调用里很常见,例如 foo().bar().baz() 中某个中间结果被借用。
10. E0106 —— 缺少生命周期标注
fn longest(x: &str, y: &str) -> &str { } // ❌判断方法:返回的引用来自哪个入参?
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { }
// 或者:如果返回的不是入参的引用,就不该返回引用
fn owned(x: &str, y: &str) -> String { }详见 生命周期。
11. E0277 —— trait 约束不满足(并发相关)
error[E0277]: `Rc<String>` cannot be sent between threads safely这是 Send / Sync 问题,不要试图绕过,换类型即可:
| 原类型 | 替换为 |
|---|---|
Rc<T> | Arc<T> |
RefCell<T> | Mutex<T> / RwLock<T> |
Cell<T> | AtomicUsize 等原子类型 |
详见 Send 与 Sync。
12. 高频场景速查
| 场景 | 症状 | 修法 |
|---|---|---|
| 循环里给集合 push | E0502 | 先收集到新 Vec,再合并;或用索引(注意失效) |
| 遍历时删除元素 | E0502 | retain()、drain(..)、mem::take |
遍历 HashMap 时修改 | E0502 | 先收集要改的键,循环外改;或用 entry API |
| ”查不到就插入” | E0502 | map.entry(k).or_insert_with(...) |
| 结构体持有自身引用 | E0106 / E0597 | 改存索引或 Rc<RefCell<T>>,或用 ouroboros 等自引用库 |
| 闭包捕获后还要用原变量 | E0382 / E0502 | 让闭包借用(去掉 move),或先取出需要的值 |
| 迭代器持有借用时修改 | E0502 | 先 collect() 成 Vec 再改 |
| 返回内部引用 | E0515 | 返回拥有型,或提供借用方法 fn get(&self) -> &T |
&mut self 方法里调另一个 &mut self 方法 | E0499 | 拆成自由函数,或把字段分开传 |
异步里跨 .await 持锁 | E0277 | 用作用域隔离,或换 tokio::sync::Mutex |
13. 辅助工具
# 静态检查会额外提示惯用写法问题
cargo clippy --all-targets -- -D warnings
# 看宏展开后的真实代码(借位报错常出在宏里)
cargo install cargo-expand
cargo expand
# 关掉 NLL 之外的老式检查(几乎不需要,了解即可)
# 绝大多数情况应该改代码,而不是改编译选项读报错的正确姿势
rustc的报错通常有help:和note:,直接指出哪两个借用冲突、在哪一行、建议怎么改。先读完再动手,比凭感觉加clone()快得多。
14. 什么时候该停下来重新设计
出现下面这些信号时,问题可能不在”借用怎么写”,而在设计:
- 一个结构体到处需要
Rc<RefCell<T>>包装 - 到处
.clone()才能编过 - 函数签名里全是
&mut参数 - 需要自引用结构体
- 生命周期参数污染了三层以上的类型
常见的重新设计方向:改成拥有型数据 + 明确的所有者;把”共享可变状态”换成消息传递;把大结构体拆成”数据 + 操作它的自由函数”;把长生命周期借用换成 ID/索引(如 Vec<T> + usize,或 slotmap)。
一句话收束
借用检查器报的绝大多数是真实的悬垂或数据竞争风险。它拦住你的时候,通常意味着原设计在某处不够清晰——先想语义,再改代码。