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: &Stringcannot 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` closurecannot move out of `self.log` which is behind a mutable reference(没错,就算是&mut也不能直接拿走)cannot move out of an `Rc`:出现在let s: String = *shared;上
这个错误是什么意思
Section titled “这个错误是什么意思”看第 7 行。这个方法返回 String,一个拥有所有权的值。所以 self.name 会把字符串从结构体里移出来,交给调用者。可是这个方法手里只有 &self,也就是对这个用户的共享(只读)借用。它并不拥有这个用户,所以不能把用户的名字送出去。
后半句 move occurs because ... does not implement the Copy trait 也很关键。如果这个字段是 u32,Rust 会悄悄复制一份,根本不会报错。而 String 拥有堆上的内存,只能移动,不能复制。
想象你下午借了朋友的书架。架上的书你随便看,但不能把书带回家:书架不是你的,朋友回来会看到一个空位。
Rust 为什么要这样做
Section titled “Rust 为什么要这样做”每个值都恰好有一个所有者,所有者离开作用域时负责清理它(见所有权)。如果你能把 self.name 从借来的 User 里移走,这个用户仍然以为自己拥有那个字符串。等用户被丢弃时,它会再释放一次那个字符串,或者有人会去读一个已经不存在的字段。这就是“释放后使用”(use after free)或“重复释放”(double free),C 和 C++ 里最糟糕的两类 bug。
Vec 也一样。names[0] 仍然归这个 vector 所有。如果 let first = names[0]; 把它移了出来,vector 在索引 0 处就会有一个空洞,而且被丢弃时还会再释放一次那个位置。所以索引只给你访问权,从不给你所有权。
一句话总结规则:通过引用,你可以读,也可以(用 &mut)修改,但不能拿走。想要所有权,就得拥有那个容器,或者在原来的位置上放一个替代品。
修复 1:借用,而不是移动
Section titled “修复 1:借用,而不是移动”只需要读取这个值时,选这个方法。大多数时候这都是正确的修法。返回 &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 和借用。
修复 2:克隆一份
Section titled “修复 2:克隆一份”当你真的需要一份属于自己的副本时,选这个方法,比如要把它存到别处,同时原来的值留在原地。几乎每条 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
- E0507 — Rust 错误代码索引(英文):官方说明,包括
mem::replace的修法。 - 什么是所有权?— Rust 程序设计语言:移动、
Clone和Copy,这个错误背后的概念。 - 引用与借用 — Rust 程序设计语言:通过
&和&mut能做什么、不能做什么。 std::mem::take— 标准库文档(英文):从&mut里拿出一个值并留下默认值;它的兄弟mem::replace让你自己选替代品。Option— 标准库文档(英文):as_ref、as_deref和take,附示例。