首页
/ 深入解析 Rust 编译错误 E0507「cannot move out of borrowed content」:成因、修复与编译器源码实现

深入解析 Rust 编译错误 E0507「cannot move out of borrowed content」:成因、修复与编译器源码实现

2026-09-07 20:36:49作者:何举烈Damon

导读

E0507 是 Rust 编译器中与所有权系统直接相关的经典错误之一,报错信息为 "cannot move out of borrowed content"(无法将值移出借用内容)。它出现的典型场景是:你通过引用(&T/&mut T)或 RefCell/闭包间接获得对某个值的访问权限,却试图把它「移动」出去——例如调用一个以 self 按值接收的方法、把被借用结构体的字段整体移出等。本篇文章以 rustc 官方错误文档 E0507.md 为骨架,结合 rustc 借用检查器(borrowck)的源码与 UI 测试,系统讲解该错误的完整成因、三类通用修复思路(不移动、收回所有权、实现 Copy)、针对可变借用成员的 mem::replace 方案,以及它在 Fn/FnMut 闭包中的特殊表现。读完你将能快速读懂 E0507 的诊断输出,理解编译器为何如此报告,并能在自己的代码中直接套用修复模式。

错误全貌:一段被移动的借用的最小复现

原文档给出的最小触发例子如下(注意方法 nothing_is_true 按值接收 self):

// compile_fail,E0507
use std::cell::RefCell;

struct TheDarkKnight;

impl TheDarkKnight {
    fn nothing_is_true(self) {}
}

fn main() {
    let x = RefCell::new(TheDarkKnight);

    x.borrow().nothing_is_true(); // error: cannot move out of borrowed content
}

这里 nothing_is_true 需要拿走 self 的所有权,但 x.borrow() 只向外部借出一个 Ref<'_, TheDarkKnight>(对 RefCell 内部内容的只读借用),因此 self 无法被移动,编译器随即报 E0507。

仓库中与该例一一对应的 UI 测试位于 tests/ui/error-codes/E0507.rs(含 //~ ERROR E0507 行内标注),其期望输出在 tests/ui/error-codes/E0507.stderr。现代 rustc 针对该例的实际诊断远比错误码标题丰富,完整输出如下:

error[E0507]: cannot move out of dereference of `Ref<'_, TheDarkKnight>`
  --> E0507.rs:12:5
   |
LL |     x.borrow().nothing_is_true();
   |     ^^^^^^^^^^ ----------------- value moved due to this method call
   |     |
   |     move occurs because value has type `TheDarkKnight`, which does not implement the `Copy` trait
   |
note: `TheDarkKnight::nothing_is_true` takes ownership of the receiver `self`, which moves value
  --> E0507.rs:6:24
   |
LL |     fn nothing_is_true(self) {}
   |                        ^^^^
note: if `TheDarkKnight` implemented `Clone`, you could clone the value
  --> E0507.rs:3:1
   |
LL | struct TheDarkKnight;
   | ^^^^^^^^^^^^^^^^^^^^ consider implementing `Clone` for this type
...
LL |     x.borrow().nothing_is_true();
   |     ---------- you could clone this value

可以看到:诊断会指出「移动发生在方法调用处」,说明「该类型未实现 Copy」这一根因,并在类型实现了 Clone 的前提下主动提示「可以先克隆这个值再调用」。

E0507 的注册与抛出位置

该错误码在 rustc_error_codes/src/lib.rserror_codes! 宏登记表中注册(第 294 行为 0507),对应解释文档即 error_codes/E0507.md。真正产生该诊断的是借用检查器(borrowck),位于 borrowck_errors.rs

pub(crate) fn cannot_move_out_of(
    &self,
    move_from_span: Span,
    move_from_desc: &str,
) -> Diag<'diag> {
    struct_span_code_err!(
        self.dcx(),
        move_from_span,
        E0507,
        "cannot move out of {}",
        move_from_desc
    )
}

其中 move_from_desc 是动态生成的路径描述,例如上面的例子里它会变成 "dereference of Ref<'_, TheDarkKnight>"。从实现细节看,E0507 报错文案是模板化的「cannot move out of {描述}」,这解释了为什么实际工程里看到的 E0507 提示词会有各种变体(如 "cannot move out of x"、"cannot move out of *cave" 等),但错误码与触发机制同源。

为什么不能从借用中移出:所有权与借用规则回顾

E0507 背后是 Rust 的两条铁律的碰撞:

  1. 移动(move)会转移所有权:把值 move 出去后,原位置不再拥有该值,其 Drop 责任与后续使用权一并移交。
  2. 借用期间禁止「掏空」被借内容:当代码握有 &T&mut T 时,它只是暂时观察/独占访问某个由他人拥有的数据;若允许从借用背后把值整体搬走,那么借用的「主人」会突然失去数据,产生悬垂引用、重复释放等内存安全问题。

原文档对此总结为:self 之所以不能被移动,是因为 .borrow() 只提供了 &TheDarkKnight——它是对 RefCell 所拥有内容的借用,而不是内容本身的所有权。因此任何需要按值消费被借内容的操作都构成 E0507。

需要注意,这条规则不仅适用于 RefCell,也不仅适用于 self 参数。从源码结构的诊断分发看,借用检查器在 move 路径分析 阶段发现任何「从不可移动位置(被借用处)发起 move」时都会走这条路径,涉及的函数集中在 move_errors.rsreport_move_errors(见 move_errors.rs#L106)及各 cannot_move_out_of 调用点(move_errors.rs),覆盖了从静态项移出、从被借用路径的子路径移出、闭包捕获变量移出等具体变体。

三种通用修复思路(附可运行示例)

原文档给出修复此类错误的三个方向,下面逐一展开并结合源码佐证。

思路一:不移动值——改接收 &self

如果方法内部并不需要真正拥有数据,把按值接收改为按借用接收即可。这是语义上最轻、也最常见的修法:

use std::cell::RefCell;

struct TheDarkKnight;

impl TheDarkKnight {
    fn nothing_is_true(&self) {} // 第一种方案:不拿走所有权
}

fn main() {
    let x = RefCell::new(TheDarkKnight);

    x.borrow().nothing_is_true(); // ok!
}

此时 self 的类型为 &TheDarkKnight,通过 x.borrow() 得到的 Ref 守卫可以被解引用后借用,无需任何 move,编译通过。这也是 Rust API 设计的通用准则:方法若能只读完成任务,就接收 &self;只有确实需要消费/改造对象本身时才接收 self&mut self

思路二:收回所有权——使用 into_inner()

若调用方确实需要调用按值方法,且此刻已不再需要保留 RefCell 中的原值,可以调用 RefCell::into_inner() 把所有权取回来,再对被取出的值调用按值方法:

use std::cell::RefCell;

struct TheDarkKnight;

impl TheDarkKnight {
    fn nothing_is_true(self) {}
}

fn main() {
    let x = RefCell::new(TheDarkKnight);
    let x = x.into_inner(); // 我们取回了所有权

    x.nothing_is_true(); // ok!
}

into_inner() 消费掉 RefCell<T> 本身并返回其中包裹的 T(前提是 RefCell 未被借用,否则运行时会 panic)。所有权回到普通局部变量 x 后,x.nothing_is_true() 的按值 self 移动完全合法。注意:这一步把 xRefCell 变成了普通 T,不再享受运行时借用检查,适合「用完即弃」的场景。

思路三:让类型可以被复制——实现 Copy

当类型的语义允许「复制一份出来用」时,为其实现 Copy(同时通常需要 Clone)即可消除 move 争议——按值方法实际消费的是值的副本,原始借用内容原封不动:

use std::cell::RefCell;

#[derive(Clone, Copy)] // 我们实现了 Copy trait
struct TheDarkKnight;

impl TheDarkKnight {
    fn nothing_is_true(self) {}
}

fn main() {
    let x = RefCell::new(TheDarkKnight);

    x.borrow().nothing_is_true(); // ok!
}

Copy 意味着赋值、传参、按值调用时发生的不是所有权转移而是按位复制,原处数据保留。编译器的诊断也与此呼应:从 E0507.stderr 可以看到,当类型未实现 Copy 时 rustc 会标注 "move occurs because value has type TheDarkKnight, which does not implement the Copy trait",并在此类型实现了 Clone 的场合给出 .clone() 的位置建议。因此在许多实际修复中,「先 #[derive(Clone, Copy)],再让编译器自动在调用点插入克隆」是机器可应用的自动建议(MachineApplicable)。

提示:Copy 只能用于所有字段均可位复制的类型。若类型包含 StringVec 等堆上所有权类型,不能直接 #[derive(Copy)],此时应退回思路一(借用)或在调用前显式 .clone()(要求实现 Clone)。

进阶场景:从可变借用结构体中移出成员

E0507 不止出现在 RefCell 场景。原文档特别指出:把一个「被可变借用(&mut)的结构体」的字段整体移出,同样触发 E0507:

// compile_fail,E0507
struct TheDarkKnight;

impl TheDarkKnight {
    fn nothing_is_true(self) {}
}

struct Batcave {
    knight: TheDarkKnight,
}

fn main() {
    let mut cave = Batcave {
        knight: TheDarkKnight,
    };
    let borrowed = &mut cave;

    borrowed.knight.nothing_is_true(); // E0507
}

表面上 borrowed&mut Batcave,持有独占的可变访问权,似乎「移动字段应该没问题」。但关键点在于:&mut 依然是借用,它指向的是 cave 所拥有的数据。一旦把 borrowed.knight 这个字段按值取出(move 走),cave 就会留下一个未初始化/悬空的字段——而借用的生命周期结束后,cave 仍需完整、可被正常析构。因此编译器拒绝这种「通过可变引用掏空宿主结构体」的操作。

mem::replace 放回一个值即可修复

原文档强调:只有当你往原位置「放回」一个值,才允许从 &mut 借用的结构体中把字段取走。标准库的 std::mem::replace 正是为此而生——它同时完成「读出旧值」与「写入新值」,保证被借结构体始终完整:

use std::mem;

struct TheDarkKnight;
impl TheDarkKnight {
    fn nothing_is_true(self) {}
}
struct Batcave {
    knight: TheDarkKnight,
}

fn main() {
    let mut cave = Batcave {
        knight: TheDarkKnight,
    };
    let borrowed = &mut cave;

    mem::replace(&mut borrowed.knight, TheDarkKnight).nothing_is_true(); // ok!
}

mem::replace(&mut borrowed.knight, TheDarkKnight) 返回原 borrowed.knight 的旧值(所有权移给调用者),同时把一个新的 TheDarkKnight 放回原字段,使 cave 自始至终保有完整的字段,于是按值调用合法。这是对「借用的可写访问 ≠ 可移动访问」这一规则的生动注解:&mut T 允许修改、甚至允许通过 replace/take 交换内容,但不允许留下空洞。标准库中 Option::takeCell::takeDefault 占位符等模式,本质上都是这种「先放回、再取走」思路的封装。

Fn / FnMut 闭包中的 E0507:按捕获方式理解

原文档还提醒:当一个类型实现了 FnFnMut 时,同样不能从它们内部移出值——因为它们通常表示「可以被调用多次」的闭包,若首次调用就把捕获变量 move 走,第二次调用将无值可用。相应的诊断文案在源码中也有直接体现,见 move_errors.rs 处注释的示例:

error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure
   |
LL |     let mut var = None;
   |         ------- captured outer variable
LL |     func(|| {
   |          -- captured by this `FnMut` closure
...

其典型代码形态是:外层变量被 FnMut 闭包以可变方式捕获(var = Some(...) 赋值把 var 移入闭包),而闭包需要被多次调用,故不允许把捕获值移动走。编译器针对这类场景,在 move_errors.rs 中实现了专门的辅助建议 suggest_clone_of_captured_var_in_move_closure

  • 仅当闭包是 move 闭包(CaptureBy::Value)且确定是捕获绑定被消费时才会给出建议;
  • 会优先找到闭包外的最近语句,插入 let value = var.clone(); 并把闭包内的使用点替换为 value
  • 若捕获变量被用于赋值目标(如 var = Some(...),见 move_errors.rs#L355-L380),克隆建议会被显式抑制,因为「克隆一个正要被赋值的值没有意义」;
  • 若无合适的插入语句,则退化为构造内联代码块 { let value = val.clone(); move || value } 的形式。

原文档同时指出:文档正文的多数讨论同样适用于非 FnOnce 闭包体。换句话说,理解 E0507 在闭包场景的关键是区分三种捕获 trait:FnOnce 可以被调用一次、因而允许把捕获值 move 走;Fn/FnMut 可能被多次调用,内部只能借用或复制捕获值,否则即 E0507。修复方向依然是老三样:不移动(改用借用/引用捕获)、收回所有权(把闭包改为 FnOnce 语义或一次性消费)、让捕获类型 Copy/Clone(必要时由编译器自动建议克隆后再移入闭包)。

边界辨析:E0507 与相邻错误码的区别

rustc 的 move 类错误并非一个码包打天下。从 borrowck_errors.rs 的源码看,与 E0507 相邻的还有:

  • E0508cannot move out of type \{ty}`, a non-copy array/slice—— 试图从**非Copy的数组或切片内部**(通过索引)移出元素。实现见cannot_move_out_of_interior_noncopy([borrowck_errors.rs#L284-L304](https://gitcode.com/GitHub_Trending/ru/rust/blob/f248f4038796913873f11ca65b1b901e311c8dae/compiler/rustc_borrowck/src/borrowck_errors.rs?utm_source=gitcode_repo_files#L284-L304)),对数组(ty::Array)与切片(ty::Slice)分别出码。这类「容器内部」的 move 因为无法通过 replace` 一次性放回完整槽位,被单独归类。
  • E0509cannot move out of type \{ty}`, which implements the `Drop` trait—— 目标类型实现了Drop,其字段的析构义务使「部分字段被移走」变得不安全。实现见 cannot_move_out_of_interior_of_drop`(borrowck_errors.rs#L306-L319)。
  • E0506cannot assign to {desc} because it is borrowed —— 这是「写」方向的冲突(给被借用变量赋值),而 E0507 是「移出」方向(从被借用处搬走),两者在 borrowck_errors.rs 中紧邻定义。

因此拿到 E0507 时应先确认「被移出对象到底是整体变量,还是某个被借用容器/结构体的内部字段」:整体移出优先考虑借用化或 Copy&mut 结构体字段移出考虑 mem::replace;数组/切片与 Drop 类型的内部移出则会以 E0508/E0509 另行报出,需要对应换用迭代器 drainOption 包装等手段。

如何在当前仓库中复现与验证

若想亲手观察 E0507 的诊断,无需额外配置环境,当前仓库已内置完备的 UI 测试:

  1. 阅读或运行最小复现用例 tests/ui/error-codes/E0507.rs,其中第 12 行的 //~ ERROR E0507 是 compiletest 的行内期望标注;
  2. 对照其期望输出 tests/ui/error-codes/E0507.stderr,体会诊断中的「move 发生点」「非 Copy 原因标注」与「Clone 建议」三部分信息;
  3. 更多实战变体分布在 borrowck 的测试目录下,例如 borrowck-move-from-subpath-of-borrowed-path(从被借用路径的子路径移出)、borrowck-move-out-of-static-item(从静态项移出)、issue-119915-bad-clone-suggestion(对错误克隆建议的回归约束)等(位于 tests/ui/borrowck 目录),可用于观察 E0507 在不同代码形态下的完整输出。

日常开发中也可直接对任意单文件运行 rustc --explain E0507,rustc 会输出本篇文章所依据的官方错误说明全文;配合 IDE 的悬停诊断,即可在编写代码的同时即时定位到「哪个位置 move、为什么该处只持有借用」。

小结:一套可复用的排错路径

把原文档的核心结论与编译器行为合并,面对任意 E0507 时按下列顺序排查即可:

  1. 先看被移出的是整体还是局部:整体变量被 &T/守卫解引用挡住 → 回到「借用接收 / into_inner / Copy」三选一;&mut 结构体的字段被移出 → 必须「放回一个值」(mem::replace 等);
  2. 再看类型语义:类型本身小而可复制 → #[derive(Clone, Copy)] 往往一步到位,且编译器在实现 Clone 时会给出自动建议;类型含所有权字段 → 放弃整体 move,改用借用或克隆;
  3. 若涉及闭包:确认闭包 trait 是 FnOnce 还是 Fn/FnMut,后者内部捕获值一律不可 move,需借用捕获或先 clone
  4. 留意相邻错误码:数组/切片与 Drop 类型内部移出分别对应 E0508、E0509,处理手段不同,不要死记同一套修法。

E0507 并非「编译器的刁难」,而是对"借用期不得掏空数据"这一内存安全约束的直接贯彻。理解它的三种修复套路与底层 borrowck 的报错机制,你就能把这类报错从「逐个试」变成「秒级定位」,写出的 API 也会更自觉地遵循 &self/self 的语义边界。

相关参考资料:E0507 官方错误文档(本文骨架)、borrowck_errors.rs(E0507 出码点)、move_errors.rs(闭包克隆建议与各变体分发)、error_codes 注册表、UI 测试 E0507.rsE0507.stderr

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
594
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
516
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388