跳转到内容

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` trait
7 | greet(name); // name moves into greet here
| ---- value moved here
8 | 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 value
help: 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”。

从上往下读这三条标注,它们讲完了整个故事:

  1. 第 6 行:name 是一个 String,而 String 不是 Copy 的。所以把它传给别处,就会移动它。
  2. 第 7 行:greet(name) 把字符串移动进了函数。现在 greet 拥有它,greet 结束时这个字符串就被丢弃了。
  3. 第 8 行:你又想读取 name,可 main 已经不再拥有它了。

就像你把家里唯一一把钥匙交给了朋友。交出去以后,你就没法用它开门了,哪怕你还记得自己曾经有过这把钥匙。

Rust 里的每个值都只有一个所有者。所有者不在了,值就会被清理掉。Rust 正是靠这一点,在没有垃圾回收器的情况下释放内存。具体规则见所有权这一页。

如果两个变量同时拥有同一个 String,最后两者都会去释放同一块内存。这叫“二次释放”(double free),是一个真实存在、会让 C 和 C++ 程序崩溃的 bug。Rust 的办法是让赋值和函数调用都移动值,这样任何时候都只有一个所有者。

i32、bool、char 这类小而简单的类型是 Copy 的:传递它们时会做一次很便宜的复制,所以它们永远不会触发 E0382。String、Vec 和大多数结构体这类拥有堆内存的类型,则会被移动。

当函数只需要看一看这个值时,选这个方法。大多数情况下这就是正确的修复。把参数改成引用,然后传入 &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。

当另一段代码确实需要它自己的一份(比如要存进结构体或 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)能解决,就优先用借用。

适合只由简单值组成的小结构体,比如坐标、颜色或 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。

当函数接收一个值、修改它,而你想要修改后的结果时,选这个方法。函数把值返回,你再把它存起来:

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