跳转到内容

Rust 错误 E0507:cannot move out of a value behind a shared reference(从借用中移出值)

E0507 的意思是:你想从一个只是借来的东西里拿走一个值(把它移动出去)。借来的值可以看,但不能带走,否则真正的所有者那里就会留下一个空洞。这个错误非常常见,尤其是在 Vec<String>、结构体字段和 Option 上。修法通常是改成借用,或者调用 .clone()。这一页先讲修法,再解释原因。这个系列的其他错误,请看 Rust 编译错误详解。

error[E0507]: cannot move out of `self.name` which is behind a shared reference
--> src/main.rs:7:9
|
7 | self.name // ❌ 想把 String 交出去,但 `self` 只是借来的
| ^^^^^^^^^ move occurs because `self.name` has type `String`, which does not implement the `Copy` trait
|
help: consider cloning the value if the performance cost is acceptable
|
7 | self.name.clone() // ❌ 想把 String 交出去,但 `self` 只是借来的
| ++++++++

第一行的意思是“self.name 位于共享引用之后,不能把它移出来”。help 建议:如果能接受性能开销,可以考虑克隆这个值。

产生这个错误的程序是这样的:

struct User {
name: String,
}
impl User {
fn name(&self) -> String {
self.name // ❌ 想把 String 交出去,但 `self` 只是借来的
}
}
fn main() {
let u = User { name: String::from("Ferris") };
println!("{}", u.name());
}

同一个错误代码有好几种说法,意思都是“这个值你只是借来的,所以不能移走它”:

  • cannot move out of index of `Vec<String>`:出现在 let first = names[0]; 这样的代码上
  • cannot move out of `*r` which is behind a shared reference:出现在 let owned = *r; 上,其中 r: &String
  • cannot move out of `nick` as enum variant `Some` which is behind a shared reference:出现在 match *nick { Some(n) => n, ... } 上
  • cannot move out of `report`, a captured variable in an `Fn` closure
  • cannot move out of `self.log` which is behind a mutable reference(没错,就算是 &mut 也不能直接拿走)
  • cannot move out of an `Rc`:出现在 let s: String = *shared; 上

看第 7 行。这个方法返回 String,一个拥有所有权的值。所以 self.name 会把字符串从结构体里移出来,交给调用者。可是这个方法手里只有 &self,也就是对这个用户的共享(只读)借用。它并不拥有这个用户,所以不能把用户的名字送出去。

后半句 move occurs because ... does not implement the Copy trait 也很关键。如果这个字段是 u32,Rust 会悄悄复制一份,根本不会报错。而 String 拥有堆上的内存,只能移动,不能复制。

想象你下午借了朋友的书架。架上的书你随便看,但不能把书带回家:书架不是你的,朋友回来会看到一个空位。

每个值都恰好有一个所有者,所有者离开作用域时负责清理它(见所有权)。如果你能把 self.name 从借来的 User 里移走,这个用户仍然以为自己拥有那个字符串。等用户被丢弃时,它会再释放一次那个字符串,或者有人会去读一个已经不存在的字段。这就是“释放后使用”(use after free)或“重复释放”(double free),C 和 C++ 里最糟糕的两类 bug。

Vec 也一样。names[0] 仍然归这个 vector 所有。如果 let first = names[0]; 把它移了出来,vector 在索引 0 处就会有一个空洞,而且被丢弃时还会再释放一次那个位置。所以索引只给你访问权,从不给你所有权。

一句话总结规则:通过引用,你可以读,也可以(用 &mut)修改,但不能拿走。想要所有权,就得拥有那个容器,或者在原来的位置上放一个替代品。

只需要读取这个值时,选这个方法。大多数时候这都是正确的修法。返回 &str 而不是 String,写 &v[0] 而不是 v[0],用 as_deref() 或 as_ref() 借用 Option 里的内容:

struct User {
name: String,
nickname: Option<String>,
}
impl User {
fn name(&self) -> &str {
&self.name
}
fn display_name(&self) -> &str {
match self.nickname.as_deref() {
Some(nick) => nick,
None => &self.name,
}
}
}
fn describe(nick: &Option<String>) -> &str {
match nick {
Some(n) => n,
None => "匿名",
}
}
fn main() {
let users = vec![
User { name: String::from("Ferris"), nickname: Some(String::from("crab")) },
User { name: String::from("Corro"), nickname: None },
];
let first = &users[0];
println!("第一个用户:{}", first.name());
for user in &users {
println!("显示为:{}", user.display_name());
}
println!("昵称:{}", describe(&users[1].nickname));
}

输出:

第一个用户:Ferris
显示为:crab
显示为:Corro
昵称:匿名
▶ 动手试一试

看 describe:它匹配的是 nick(引用本身),而不是 *nick。这样 n 就是一个 &String,也就是借用,什么都没有移动。如果写成 match *nick { Some(n) => ... },就是要求 n 拿到一个拥有所有权的 String,于是出现 E0507。编译器的提示也是这么说的:consider removing the dereference here(考虑去掉这里的解引用)。

as_deref() 把 &Option<String> 变成 Option<&str>;as_ref() 得到的是 Option<&String>。在 .unwrap()、.map() 或 match 之前先调用它们,你处理的就是借用,而不是拥有所有权的值。r: &String 时的 *r 也一样:通常根本不需要 *,直接用 r 就行。参见 Option 和借用。

当你真的需要一份属于自己的副本时,选这个方法,比如要把它存到别处,同时原来的值留在原地。几乎每条 E0507 信息里,编译器都会这样建议:

struct User {
name: String,
}
impl User {
fn name(&self) -> String {
self.name.clone()
}
}
fn main() {
let names = vec![String::from("Ferris"), String::from("Corro")];
let first = names[0].clone();
println!("{first} 排第一,列表里还有 {} 个名字", names.len());
let u = User { name: String::from("Ferris") };
let mut badge = u.name();
badge.push_str("(管理员)");
println!("{badge} / {}", u.name);
}

输出:

Ferris 排第一,列表里还有 2 个名字
Ferris(管理员) / Ferris
▶ 动手试一试

克隆一个名字或一个短列表开销很小,完全没问题,学习阶段尤其如此。只有在热点循环里处理大量数据时,它才会成为问题。如果借用(修复 1)就够用,优先用借用。

修复 3:拿出来,并留下一个替代品

Section titled “修复 3:拿出来,并留下一个替代品”

当你有 &mut 访问权,并且想真正把值移出来时,选这个方法,比如交出一份写好的日志,或者取出下一个任务。你不能留下空洞,但可以换一个替代品进去:

  • std::mem::take(&mut x) 拿走这个值,留下它的默认值(String 是 "",Vec 是空的)。
  • option.take() 拿走 Option 里的内容,留下 None。
  • std::mem::replace(&mut x, new) 把 new 放进去,把旧值还给你。
use std::mem;
#[derive(Debug)]
enum Status {
Queued,
Running,
}
struct Job {
log: String,
next_step: Option<String>,
status: Status,
}
impl Job {
fn take_log(&mut self) -> String {
mem::take(&mut self.log)
}
fn start_next(&mut self) -> Option<String> {
self.next_step.take()
}
fn set_status(&mut self, new: Status) -> Status {
mem::replace(&mut self.status, new)
}
}
fn main() {
let mut job = Job {
log: String::from("build ok"),
next_step: Some(String::from("deploy")),
status: Status::Queued,
};
let old = job.set_status(Status::Running);
println!("状态:{old:?} -> {:?}", job.status);
println!("下一步:{:?}", job.start_next());
println!("再取下一步:{:?}", job.start_next());
println!("日志:{:?}", job.take_log());
println!("现在的日志:{:?}", job.log);
}

输出:

状态:Queued -> Running
下一步:Some("deploy")
再取下一步:None
日志:"build ok"
现在的日志:""
▶ 动手试一试

结构体里从来不会留下空洞:take_log 之后,日志变成空字符串;start_next 之后,下一步变成 None。这是修复“cannot move out of self.x which is behind a mutable reference”的标准做法。mem::take 要求类型实现了 Default;其他类型就用 mem::replace。

修复 4:拥有集合,再从里面移出来

Section titled “修复 4:拥有集合,再从里面移出来”

当你不再需要这个集合或结构体,只想要它里面的内容时,选这个方法。如果 Vec 归你所有,它就能把元素交出来:remove(i)、swap_remove(i)、pop(),或者用 into_iter() 一次全部交出。接收 self(而不是 &self)的方法拥有这个结构体,可以把它的字段移出去:

struct User {
name: String,
}
impl User {
fn into_name(self) -> String {
self.name
}
}
fn main() {
let mut queue = vec![String::from("email"), String::from("backup"), String::from("report")];
let first = queue.remove(0);
println!("正在处理 {first},还剩:{queue:?}");
let users = vec![User { name: String::from("Ferris") }, User { name: String::from("Corro") }];
let names: Vec<String> = users.into_iter().map(|u| u.into_name()).collect();
println!("{names:?}");
}

输出:

正在处理 email,还剩:["backup", "report"]
["Ferris", "Corro"]
▶ 动手试一试

into_iter() 会消耗掉这个 vector,所以之后就不能再用 users 了(否则就是 E0382)。into_name 的 into_ 前缀是 Rust 的命名习惯,它告诉读代码的人:“这个方法会用掉这个值”。remove(0) 会把后面的元素都往前挪一位;如果不在乎顺序,swap_remove 更快。参见删除元素。

在遍历 &users 的 for 循环里收集字段。 for u in &users { names.push(u.name); } 会报 cannot move out of `u.name` which is behind a shared reference。修法:names.push(u.name.clone());如果之后不再需要 users,就写成 for u in users;或者只收集借用:users.iter().map(|u| u.name.as_str()).collect::<Vec<&str>>()。

通过引用对 Option 字段调用 .unwrap()。 当 config: &Config 时,config.title.unwrap() 会失败,因为 unwrap 按值接收 Option。编译器会给出修法:config.title.as_ref().unwrap()(得到 &String),或者用 config.title.as_deref() 得到 Option<&str>。

在 Fn 闭包里移动捕获的变量。 一个可能被调用很多次的闭包,不能把它捕获的东西送出去:cannot move out of `report`, a captured variable in an `Fn` closure。修法:在闭包里借用(&report),或者在闭包里克隆;如果它只会运行一次,就把约束改成 FnOnce。参见 Fn、FnMut 和 FnOnce。

解引用 Rc。 let s: String = *shared; 会报 cannot move out of an `Rc`,因为其他所有者可能还在用这个值。修法:借用它(&*shared 或直接 &shared)、克隆它,或者用 Rc::unwrap_or_clone(shared)。Box 不一样:它归你所有,所以 let s = *boxed; 可以编译。参见智能指针。

  • E0382:使用已移动的值。移动本身是允许的,但之后你又用了这个值。
  • E0594:不能通过 & 引用赋值。同样是“你只是借来的”这条规则,只不过针对的是写入,而不是移动。
  • E0596:不能借用为可变,比如在只有 &self 的方法里用了 mem::take(&mut self.x)。
  • E0308:类型不匹配,expected String, found &String,常常在改成借用之后马上出现。返回 &str,或者调用 .clone()。
  • E0515:不能返回局部变量的引用,当基于借用的修法指向一份马上就要被丢弃的数据时出现。
  • 所有编译错误:全部错误页面的列表。

小测验

1. names: Vec<String> 时,let first = names[0]; 报 E0507。如果你只是要打印 first,通常最好的修法是什么?

2. 方法 fn next(&mut self) -> Option<Task> 要交出 self.pending(一个 Option<Task>)。哪种写法能编译?

3. 为什么 nums: Vec<i32> 时 let n = nums[0]; 没问题,换成 Vec<String> 就不行?

得分: 0 / 3