Rust 错误 E0382:borrow of moved value(使用已移动的值)
E0382 的意思是:你把一个值交出去了(也就是移动了它),然后又想再用它。这是 Rust 新手第一周最常遇到的错误,而且几乎总是改一行就能解决。本页先给出修复方法,再解释背后发生了什么。这个系列的其他错误,请看 Rust 编译错误详解。
error[E0382]: borrow of moved value: `name` --> src/main.rs:8:25 |6 | let name = String::from("Ferris"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait7 | greet(name); // name moves into greet here | ---- value moved here8 | println!("Goodbye, {name}!"); // ❌ name is gone | ^^^^ value borrowed here after move |note: consider changing this parameter type in function `greet` to borrow instead if owning the value isn't necessary --> src/main.rs:1:16 |1 | fn greet(name: String) { | ----- ^^^^^^ this parameter takes ownership of the valuehelp: consider cloning the value if the performance cost is acceptable |7 | greet(name.clone()); // name moves into greet here | ++++++++产生这个错误的程序是这样的:
fn greet(name: String) { println!("Hello, {name}!");}
fn main() { let name = String::from("Ferris"); greet(name); // name moves into greet here println!("Goodbye, {name}!"); // ❌ name is gone}编译器说:name 的值已经被移动走了,之后又被借用(“value borrowed here after move”)。note: 提示你,如果 greet 不需要拥有这个值,可以把参数改成借用;help: 则建议在性能开销可以接受时克隆这个值。
你也可能看到“use of moved value”这种说法,它是同一个错误。如果后面那行只是读取这个值(比如 println!),Rust 就说“borrow”;如果它想再次移动这个值(比如 let other = name;),就说“use”。
这个错误是什么意思
Section titled “这个错误是什么意思”从上往下读这三条标注,它们讲完了整个故事:
- 第 6 行:
name是一个String,而String不是Copy的。所以把它传给别处,就会移动它。 - 第 7 行:
greet(name)把字符串移动进了函数。现在greet拥有它,greet结束时这个字符串就被丢弃了。 - 第 8 行:你又想读取
name,可main已经不再拥有它了。
就像你把家里唯一一把钥匙交给了朋友。交出去以后,你就没法用它开门了,哪怕你还记得自己曾经有过这把钥匙。
Rust 为什么要这样做
Section titled “Rust 为什么要这样做”Rust 里的每个值都只有一个所有者。所有者不在了,值就会被清理掉。Rust 正是靠这一点,在没有垃圾回收器的情况下释放内存。具体规则见所有权这一页。
如果两个变量同时拥有同一个 String,最后两者都会去释放同一块内存。这叫“二次释放”(double free),是一个真实存在、会让 C 和 C++ 程序崩溃的 bug。Rust 的办法是让赋值和函数调用都移动值,这样任何时候都只有一个所有者。
i32、bool、char 这类小而简单的类型是 Copy 的:传递它们时会做一次很便宜的复制,所以它们永远不会触发 E0382。String、Vec 和大多数结构体这类拥有堆内存的类型,则会被移动。
修复 1:用 & 借用
Section titled “修复 1:用 & 借用”当函数只需要看一看这个值时,选这个方法。大多数情况下这就是正确的修复。把参数改成引用,然后传入 &name:
fn greet(name: &str) { println!("Hello, {name}!");}
fn main() { let name = String::from("Ferris"); greet(&name); // 借出去,而不是送出去 println!("Goodbye, {name}!"); // ✅ 还是我们的}输出:
Hello, Ferris!Goodbye, Ferris!写成 &String 也行,但通常用 &str,因为它还能接受字符串字面量。参见借用和字符串。如果函数需要修改这个值,就改用 &mut。
修复 2:克隆一份
Section titled “修复 2:克隆一份”当另一段代码确实需要它自己的一份(比如要存进结构体或 Vec),而你还要继续用原来的值时,选这个方法。.clone() 会做一份完整、独立的副本:
struct Player { name: String, score: u32,}
fn main() { let name = String::from("Ferris"); let player = Player { name: name.clone(), score: 0 }; println!("{} 的初始分数是 {} 分", player.name, player.score); println!("原来的名字:{name}");}输出:
Ferris 的初始分数是 0 分原来的名字:Ferris说句实在话:克隆一个短字符串或一个小 Vec 开销很小,完全没问题,学习阶段更是如此。只有在热点循环里克隆大块数据时才需要操心。如果借用(修复 1)能解决,就优先用借用。
修复 3:给小类型 derive Copy
Section titled “修复 3:给小类型 derive Copy”适合只由简单值组成的小结构体,比如坐标、颜色或 ID。加上 #[derive(Clone, Copy)],这个结构体就会被复制而不是移动:
#[derive(Debug, Clone, Copy)]struct Point { x: i32, y: i32,}
fn show(p: Point) { println!("({}, {})", p.x, p.y);}
fn main() { let start = Point { x: 0, y: 3 }; show(start); // 复制了 start show(start); // ✅ 还能再用 println!("{:?}", start);}输出:
(0, 3)(0, 3)Point { x: 0, y: 3 }只有当每个字段也都是 Copy 时,Copy 才能用。带有 String 或 Vec 字段的结构体不能是 Copy,这时请用 Clone 并调用 .clone()。详见常用 trait。
修复 4:把所有权交还回来
Section titled “修复 4:把所有权交还回来”当函数接收一个值、修改它,而你想要修改后的结果时,选这个方法。函数把值返回,你再把它存起来:
fn add_signature(mut letter: String) -> String { letter.push_str("\n—— Ferris"); letter // 交还回去}
fn main() { let letter = String::from("亲爱的 Rustacean:"); let letter = add_signature(letter); // 再拿回来 println!("{letter}");}输出:
亲爱的 Rustacean:—— Ferris这种写法很适合“构建器”(builder)风格的函数。如果只是原地修改一下,写成 fn add_signature(letter: &mut String) 往往更好读。
用 for 循环遍历 Vec。 for task in tasks 会把整个 vector 移动进循环,所以循环结束后就不能再用 tasks 了:
for task in tasks { println!("Doing: {task}");}println!("{} tasks done", tasks.len()); // ❌ `tasks` moved due to this implicit call to `.into_iter()`修复:遍历它的借用,写成 for task in &tasks。参见 for 循环和迭代器。
把值放进 Vec 或结构体。 names.push(name) 和 Order { customer } 都会把值移动进去,之后再用 name 或 customer 就会报错。修复:从它的新位置读回来(names[0]、order.customer),把其他操作挪到移动之前,或者 push 一个 .clone()。
在循环里移动。 如果 log.push(message) 写在循环里,第一轮就把 message 移动走了,第二轮就什么都不剩了。编译器会说 value moved here, in previous iteration of loop(值在上一轮循环里被移动了)。修复:push message.clone(),或者在循环内部创建这个值,让每一轮都有一个新的。
线程和 move 闭包。 thread::spawn(move || ...) 会把捕获的每个变量都移动进新线程。修复:在创建线程之前先克隆数据,或者用 Arc 共享它。参见并发和共享状态。
- E0502:你借用了一个值,然后在借用还在使用时又想修改它。
- E0499:同一时间对同一个值有两个可变借用。
- E0106:缺少生命周期标注,常常出现在你为了避免移动而返回引用的时候。
- E0308:类型不匹配,比如改成借用之后,在需要
&str的地方传了String。 - 所有编译错误:错误页面的完整列表。
小测验
1. report 是一个 String,你调用了 save(report),然后打印 report。为什么会出现 E0382?
2. 一个函数只需要读取一个 Vec<String>。通常最好的修复是什么?
3. 下面哪个结构体可以用 #[derive(Clone, Copy)]?
得分: 0 / 3
- E0382 — Rust error codes index(英文):这个错误的官方解释,附有更多示例。
- 什么是所有权? — Rust 程序设计语言:一步步讲解移动、
Clone和Copy的章节。 - 引用与借用 — Rust 程序设计语言:
&怎样让你在不移动值的情况下使用它。 - 所有权和移动 — 通过例子学 Rust:关于移动和部分移动的简短可运行示例。
Copytrait — 标准库文档(英文):哪些类型是Copy的,以及Copy和Clone的区别。