借用检查器常见错误清单

用途

这是一篇排错手册:编译报错时按错误码查。每个错误都给出「症状 → 原因 → 修法」,修法按推荐优先顺序排列。

核心心智三问,任何借用问题都先问这三句:

  1. 谁拥有这个值?
  2. 现在有谁在借它?(不可变还是可变)
  3. 这个借用要活到什么时候?(最后一次使用点在哪)

0. 修法的优先顺序

不要第一反应就 clone()

按这个顺序考虑,前一步能解决就不要走到下一步:

  1. 改设计:把所有权转移到需要的地方,或让函数返回拥有型值
  2. 缩短作用域:把借用限制在更小的 {} 里,或调整语句顺序(NLL 会帮忙)
  3. 改成借用:&T / &mut T / &str / &[T]
  4. 用标准库工具:split_at_mut、mem::take、mem::replace、entry API、retain、drain
  5. clone():确实是需要独立副本时
  6. 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 immutable
let 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 reference
fn 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 变成空 String

mem::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. 高频场景速查

场景症状修法
循环里给集合 pushE0502先收集到新 Vec,再合并;或用索引(注意失效)
遍历时删除元素E0502retain()、drain(..)、mem::take
遍历 HashMap 时修改E0502先收集要改的键,循环外改;或用 entry API
”查不到就插入”E0502map.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)。

一句话收束

借用检查器报的绝大多数是真实的悬垂或数据竞争风险。它拦住你的时候,通常意味着原设计在某处不够清晰——先想语义,再改代码。

相关笔记