Skip to content

常见坑与调试方法论

高频借用检查报错的成因与修法,以及一套可复用的排查流程。

常见坑与编译错误

下面每条都是 rustc 1.98.1 --edition 2024 实测输出(行号来自最小复现文件)。

E0382:borrow of moved value

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}", s1);
}
error[E0382]: borrow of moved value: `s1`
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{}", s1);
  |                    ^^ value borrowed here after move
help: consider cloning the value if the performance cost is acceptable
  • 原因String 不实现 Copylet s2 = s1; 是移动;s1 已失去所有权。
  • 修法(按优先级):
rust
fn main() {
    let s1 = String::from("hello");

    // 后面还要用内容 ⇒ 借用,零成本(首选)
    let n = s1.len();
    println!("{s1} 有 {n} 个字节");

    // 真的需要两个独立的所有者 ⇒ 显式深拷贝
    let s3 = s1.clone();
    println!("{s1} / {s3}");

    // 只是想把它交给下游 ⇒ 直接 move,别 clone(这里 s1 交出去后不再用)
    let s4 = s1;
    println!("{s4}");
}

⚠️ 陷阱for x in vec 会 move 掉 vec,循环后就再也用不了。 只想读就写 for x in &vec,想改就写 for x in &mut vec

E0502:cannot borrow as mutable because it is also borrowed as immutable

rust
fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    let r3 = &mut s;
    r3.push_str(" world");
    println!("{}", r1);
}
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  |
3 |     let r1 = &s;
  |              -- immutable borrow occurs here
...
6 |     let r3 = &mut s;
  |              ^^^^^^ mutable borrow occurs here
8 |     println!("{}", r1);
  |                    -- immutable borrow later used here
  • 原因:不可变借用 r1 的最后一次使用在第 8 行,晚于第 6 行的可变借用。
  • 修法:把只读阶段收成一个块,让它在可变借用前结束。
rust
fn main() {
    let mut s = String::from("hello");
    {
        let r1 = &s;
        let r2 = &s;
        println!("{} and {}", r1, r2);     // 输出:hello and hello
    }                                       // 只读阶段到此结束
    let r3 = &mut s;
    r3.push_str(" world");
    println!("{r3}");                       // 输出:hello world
}

循环里 Vec::push 与迭代借用冲突(同一个 E0502 的子类,出现频率最高):

rust
fn main() {
    let mut v = vec![1, 2, 3];
    for x in &v {
        if *x == 2 {
            v.push(9);
        }
    }
}
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
  |
3 |     for x in &v {
  |              --
  |              |  immutable borrow occurs here
  |              |  immutable borrow later used here
4 |         if *x == 2 { v.push(9); }
  |                      ^^^^^^^^^ mutable borrow occurs here
  • 原因&v 的迭代器在整个循环期间持有不可变借用;push 需要可变借用。 而且 push 可能让 Vec 重新分配缓冲区,正在迭代的指针会立刻失效 —— 这正是 C++ 里经典的迭代器失效 UB,Rust 把它拦在了编译期。
  • 修法 A(收集后再改):先只读地算出「要加什么」,循环结束后再改。
rust
fn main() {
    let mut v = vec![1, 2, 3];
    let to_add: Vec<i32> = v.iter().filter(|&&x| x == 2).map(|_| 9).collect();
    v.extend(to_add);
    println!("{v:?}");          // 输出:[1, 2, 3, 9]
}
  • 修法 B(用索引遍历)0..v.len() 每次重新取长度,索引访问只做短借用。
rust
fn main() {
    let mut v = vec![1, 2, 3];
    for i in 0..v.len() {
        if v[i] == 2 {
            v.push(9);
        }
    }
    println!("{v:?}");          // 输出:[1, 2, 3, 9]
}

⚠️ 陷阱:修法 B 在这里能编过,但语义上是个陷阱v.len() 在每次循环 重新求值,push 会让循环继续跑(这里因为新加的元素不等于 2 才停下)。 如果条件能匹配新元素,就是死循环 + 内存爆掉。要在循环前把长度存下来: let n = v.len(); for i in 0..n { ... }

  • 修法 C(按值取走要处理的数据)std::mem::takeVec 换成空的, 处理完后写回。见「第五步:把「借用问题」转成「所有权问题」」。

E0499:cannot borrow as mutable more than once at a time

rust
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{}, {}", r1, r2);
}
error[E0499]: cannot borrow `s` as mutable more than once at a time
  |
3 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
4 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
5 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here
  • 原因:可变引用要求独占;两个 &mut s 同时活着就破坏了排他性。
  • 修法:让两个可变借用在时间上串行(前一个用完,后一个才出现)。
rust
fn main() {
    let mut s = String::from("hello");
    {
        let r1 = &mut s;
        r1.push_str(", world");
    }                                  // r1 结束
    let r2 = &mut s;
    r2.push('!');
    println!("{r2}");                  // 输出:hello, world!
}

💡 对照:C++ 里两个 T& 同时指向一个对象完全合法, 于是 f(x, x) 这种调用是否安全取决于 f 的实现 —— Rust 用 E0499 强制你把意图写清楚。

E0505:cannot move out of s because it is borrowed

rust
fn main() {
    let s = String::from("hello");
    let r = &s;
    let t = s;
    println!("{}", r);
}
error[E0505]: cannot move out of `s` because it is borrowed
  |
2 |     let s = String::from("hello");
  |         - binding `s` declared here
3 |     let r = &s;
  |             -- borrow of `s` occurs here
4 |     let t = s;
  |             ^ move out of `s` occurs here
5 |     println!("{}", r);
  |                    - borrow later used here
help: consider cloning the value if the performance cost is acceptable
  • 原因r 还指向 s 里的堆缓冲区。如果允许把 s move 走, 被 move 的那份数据会在新所有者的作用域结束时被 drop,r 就悬垂了。
  • 修法:调换顺序,先 move 再(或不)借用;把 r 用完再 move; 用 r.clone() / s.clone() 断开关系。
rust
fn main() {
    let s = String::from("hello");

    // 只读阶段先结束
    println!("{}", &s);

    // 再转移所有权
    let t = s;
    println!("{t}");                   // 输出:hello
}

E0515:cannot return reference to local variable

rust
struct Foo {
    name: String,
}

fn make<'a>() -> &'a Foo {
    let f = Foo { name: String::from("a") };
    &f
}

fn main() {
    let _ = make();
}
error[E0515]: cannot return reference to local variable `f`
  |
5 |     &f
  |     ^^ returns a reference to data owned by the current function
  • 原因f 在函数返回时被 drop,返回的引用会悬垂。注意报错码和 E0106 的区别: 写了生命周期参数之后才轮到 E0515
  • 修法:返回拥有所有权的值,让所有权随返回值移出函数。
rust
struct Foo {
    name: String,
}

fn make() -> Foo {
    Foo { name: String::from("a") }     // 所有权移出,函数内不发生 drop
}

fn main() {
    let f = make();
    println!("{}", f.name);             // 输出:a
}

如果调用方确实只想「看一眼」,让调用方持有所有者、函数返回借用:

rust
struct Foo {
    name: String,
}

fn name_of(f: &Foo) -> &str {           // 规则 2 省略:输出跟着唯一的输入引用
    &f.name
}

fn main() {
    let f = Foo { name: String::from("a") };
    println!("{}", name_of(&f));        // 输出:a
}

E0716:temporary value dropped while borrowed

rust
fn main() {
    let r = format!("hello {}", 1).as_str();
    println!("{}", r);
}
error[E0716]: temporary value dropped while borrowed
  |
2 |     let r = format!("hello {}", 1).as_str();
  |             ^^^^^^^^^^^^^^^^^^^^^^         - temporary value is freed at the end of this statement
  |             |
  |             creates a temporary value which is freed while still in use
3 |     println!("{}", r);
  |                    - borrow later used here
  = note: consider using a `let` binding to create a longer lived value
  • 原因format!(...) 产生的 String 是一个临时值, 它在整条语句结束时就被 drop;而 r 借用了它内部的缓冲区。
  • 修法:用 let 绑定把临时值提升为一个具名变量,让它的作用域覆盖 r 的使用。
rust
fn main() {
    let s = format!("hello {}", 1);     // 具名绑定,活到 main 结束
    let r = s.as_str();
    println!("{r}");                    // 输出:hello 1
}

🧠 原理:临时值生命周期延长(temporary lifetime extension)在少数固定语法位置 会自动发生 —— 例如 let r = &String::from("x"); 是合法的,因为 &<临时值> 直接作为 let 的初始化表达式会被延长。但方法调用链 .as_str() 之后的借用不延长。 遇到 E0716 最省事的办法永远是:加一个 let 绑定。

E0106:missing lifetime specifier

rust
fn main() {
    let s = no_dangle();
    println!("{}", s);
}

fn no_dangle() -> &String {
    let s = String::from("hello");
    &s
}
error[E0106]: missing lifetime specifier
  |
6 | fn no_dangle() -> &String {
  |                   ^ expected named lifetime parameter
  = help: this function's return type contains a borrowed value,
          but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're
      returning a borrowed value from a `const` or a `static`
help: instead, you are more likely to want to return an owned value
  • 原因:函数签名里出现了一个引用返回值,但省略规则(见「生命周期省略规则(elision)」)无法为它找到来源 —— 输入位置没有任何引用参数
  • 修法:返回拥有所有权的 String(绝大多数情况的正解); 若有引用参数,按规则 2 自动省略;有两个以上引用参数且不确定借谁,手写 'a
rust
fn no_dangle() -> String {
    String::from("hello")
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    println!("{}", no_dangle());                          // 输出:hello
    println!("{}", longest("abcd", "xy"));                // 输出:abcd
}

⚠️ 陷阱不要靠加 'static 来消错。fn f() -> &'static String 只是换了个 错误(String 不是 'static 数据),并且把错误的承诺写进了 API。 同理,Box::leak 能造出 'static 引用,但那等于故意泄漏内存, 只在初始化全局配置这类一次性场景才合理。

附带三个高频小坑

(1) E0596:改内容却没声明 mut

rust
fn main() {
    let s = String::from("hello");
    s.push_str(" world");
}
// error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
// help: consider changing this to be mutable
//   2 |     let mut s = String::from("hello");

💡 对照:Java 的局部变量没有「可变性」概念;Python 变量名可以随时重绑。 Rust 把「这个绑定允许被改」也写进声明:let mut s。 注意 let mut smut绑定的可变性,不是值类型的一部分 —— 所以 let s = String::from("x"); let mut t = s; 是合法的:t 可变,s 已经作废。

(2) 结构体字段的 mut 由绑定决定

rust
struct Point { x: i32, y: i32 }

fn main() {
    let mut p = Point { x: 1, y: 2 };
    p.x = 10;                      // 合法:p 是 mut 绑定
    println!("{} {}", p.x, p.y);   // 输出:10 2

    let q = Point { x: 1, y: 2 };
    // q.x = 10;                   // error[E0594]: cannot assign to `q.x`, as `q` is not declared as mutable
    let _ = q;
}

Rust 没有 C++ 的 mutable 成员修饰(除非用内部可变性类型), 也没有 Java 的「字段级 final」——可变性挂在绑定上,不挂在字段上。

(3) 部分移动(partial move)

rust
struct Person {
    name: String,
    age: u8,
}

fn main() {
    let p = Person { name: String::from("Ann"), age: 20 };
    let n = p.name;                 // 只把 name 移走,Person 变成「部分移动」
    println!("{n} {}", p.age);      // 合法:age 是 Copy,还能读
    // println!("{}", p.name);      // error[E0382]: borrow of moved value: `p.name`
    // let q = p;                   // error[E0382]: use of partially moved value: `p`
}

⚠️ 陷阱:一旦结构体发生部分移动,整个结构体就不能整体 move 或整体借用, 但未移动的字段仍可用。要避免这种状态,用 .. 解构或者一开始就 clone



借用检查器调试方法论

借用检查器的报错通常不是「你写错了语法」,而是「你的数据结构或接口设计有不清晰的所有权」。 下面这套流程按成本从低到高排列,照着顺序试,不要跳步

第一步:读 borrow later used here 那一行

几乎所有 E0502/E0499/E0505 的答案都在这行里。它告诉你这个借用活到了哪一行。 先问自己:第 8 行真的需要用 r1 吗? 如果不需要,直接删掉那个引用。

第二步:缩小作用域

把「只读阶段」和「可写阶段」用 {} 显式分开。这是最高性价比的修法,零运行期成本。

rust
fn main() {
    let mut data = vec![10, 20, 30];

    // 只读阶段:算出一个结论,然后立刻放掉借用
    let total: i32 = {
        let view = &data;
        view.iter().sum()
    };                                  // view 到这里结束

    // 可写阶段:现在没人借了
    data.push(total);
    println!("{data:?}");               // 输出:[10, 20, 30, 60]
}

第三步:先 clone() 跑通,再优化

借用检查器卡住时,先 clone()逻辑逼近正确,再逐处把 clone() 换成借用。 这比一边调逻辑一边调借用容易得多。

rust
fn main() {
    let mut words = vec![String::from("a"), String::from("b")];

    // 第一版:让它先编过(有代价:克隆了整个 Vec)
    let snapshot = words.clone();
    for w in &snapshot {
        if w == "a" {
            words.push(String::from("c"));
        }
    }
    println!("{words:?}");              // 输出:["a", "b", "c"]

    // 第二版:把 clone 换成「先收集要加的东西」
    let mut more = Vec::new();
    for w in &words {
        if w == "a" {
            more.push(String::from("d"));
        }
    }
    words.append(&mut more);
    println!("{words:?}");              // 输出:["a", "b", "c", "d"]
}

⚠️ 陷阱clone() 不是「作弊」,但必须回头处理。 要么当场优化成借用,要么在注释里写明为什么这里必须拥有所有权。

第四步:跑 cargo clippy

clippy 会指出「你写了 clone() 但其实不需要」「你用了 &String 参数」这类问题。

powershell
cargo clippy --all-targets -- -D warnings
cargo clippy --fix --allow-dirty          # 自动修掉可安全修复的部分

常用相关 lint:

lint提示内容修法
clippy::ptr_arg参数写成 &String / &Vec<T>改成 &str / &[T]
clippy::redundant_cloneclone() 出来的值其实没被独立使用删掉 clone()
clippy::needless_borrow多写了一个 &(如 &x 处已经是引用)删掉
clippy::clone_on_copyCopy 类型调用 .clone()直接赋值

第五步:把「借用问题」转成「所有权问题」

有些冲突靠缩作用域解决不了,因为冲突的两方在语义上真的需要同时存在。 这时用 std::mem 家族把值临时搬出去,让借用消失:

rust
fn main() {
    let mut buffer = vec![1, 2, 3];

    // std::mem::take:把 buffer 换成 Default::default()(空 Vec),返回原值
    let mut taken = std::mem::take(&mut buffer);
    for x in &taken {
        buffer.push(x * 10);            // buffer 现在是空的,没有任何借用冲突
    }
    taken.clear();
    buffer.append(&mut taken);
    println!("{buffer:?}");             // 输出:[10, 20, 30]
}
工具作用典型场景
std::mem::take(&mut x)取出 x,留 T::default()VecStringOption 的「清空并用」
std::mem::replace(&mut x, new)取出 x,放入 new状态机状态切换、树节点旋转
std::mem::swap(&mut a, &mut b)交换两个值排序、就地翻转
Option::take()&mut Option<T> 变成 Option<T>,原位留 None单次初始化、递归里的「取出再放回」
Rc<RefCell<T>>把借用检查推到运行期单线程图结构(〈智能指针与闭包〉一章)

🧠 原理take/replace 之所以有效,是因为它们把「共享借用」变成了「独占所有权」: 值搬到局部变量后,原变量处于「空」状态,任何对它其他字段的借用都合法了。 代价是一次移动(对 Vec 就是 24 字节)。

何时该用索引代替迭代器

迭代器是 Rust 的惯用写法,但在「边遍历边改同一个容器」时它表达不了意图, 这时索引是正当选择 —— 代价是每次 v[i] 都做边界检查(bounds check)

rust
fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    // 索引写法:能边遍历边改,但每次访问都有边界检查
    for i in 0..v.len() {
        if v[i] % 2 == 0 {
            v[i] *= 100;
        }
    }
    println!("{v:?}");                  // 输出:[1, 200, 3, 400, 5]

    // 迭代器 + 可变引用:无索引、边界检查可被优化掉(惯用且通常更快)
    for x in v.iter_mut() {
        if *x % 2 != 0 {
            *x += 1;
        }
    }
    println!("{v:?}");                  // 输出:[2, 200, 4, 400, 6]
}

性能代价说明(不要凭感觉下结论):

  • 迭代器(iter() / iter_mut())在优化后通常能把边界检查完全消除, 因为长度已知且索引由迭代器内部维护;也更容易被向量化(auto-vectorization)。
  • 索引写法每次 v[i] 都执行一次 i < v.len() 检查,且可能触发 panic 分支, 从而妨碍向量化。但优化器有时能通过 range 分析把检查提出来。
  • 结论:默认写迭代器;只有借用规则不允许时才用索引, 若该循环在性能热点上,请用 criterion 之类做基准测试(benchmark)来验证, 而不是靠直觉或「听说」。
  • 想显式消除边界检查又必须用索引时,可用 iter_mut().enumerate() —— 既拿到索引,又让借用检查器满意。
rust
fn main() {
    let mut v = vec![10, 20, 30];
    for (i, x) in v.iter_mut().enumerate() {
        if i == 1 {
            *x += 1;                    // 索引与可变借用同时到手
        }
    }
    println!("{v:?}");                  // 输出:[10, 21, 30]
}

⚠️ 陷阱for i in 0..v.len()v.len() 每轮重新求值。 循环里 push 会让循环一直跑下去。除非你故意要处理新加入的元素, 否则永远先 let n = v.len();for i in 0..n



速查表

主题要点
三条规则每个值有唯一所有者;同一时刻只有一个所有者;所有者离开作用域即 drop
let b = a;a: Copy ⇒ 复制(两者可用);否则 move(a 作废,E0382
Copy 类型整数、浮点、boolchar&T、元组/数组(元素全 Copy)、fn 指针
CopyStringVec<T>Box<T>&mut TFile、任何有 Drop 的类型
CopyDrop互斥:有析构函数就不能 CopyE0184);字段不 Copy 也不能 CopyE0204
传参值传递 = move(除非 Copy);&T = 只读借用;&mut T = 独占借用
借用规则同一作用域内:多个 &T 恰好一个 &mut T,二者不可共存
借用何时结束最后一次使用判定(NLL,2018 edition 起);不是花括号
省略规则 1每个省略生命周期的输入引用各得一个参数
省略规则 2只有一个输入生命周期时,输出引用与它相同
省略规则 3&self / &mut self 时,输出引用与 self 相同
结构体带引用必须写 struct S<'a> { f: &'a str };省略规则不适用于结构体定义
参数类型选择&str > &String&[T] > &Vec<T>;返回引用才需要生命周期
&'static str字符串字面量;不要用它消 E0106
返回内部引用不可能(E0515);返回拥有所有权的值,或返回借用参数的引用
边遍历边改迭代器会持借用 ⇒ E0502;改用「先收集后改」、索引、或 mem::take
内部可变性Cell(按值)、RefCell(运行期借用检查,panic)、Rc(多所有者、不可变)
线程安全Rc/RefCell 不是;Arc/Mutex/RwLock
编译错误速记E0382 移动后用 / E0502 读写在争 / E0499 双可变 / E0505 借用中移动 / E0515 返回局部引用 / E0716 临时值太短 / E0106 缺生命周期


内容以 rustc 1.98.1 · Rust 2024 edition 为基准