首页
/ rustc 错误码 E0504 全解:不能把被借用的变量移入闭包

rustc 错误码 E0504 全解:不能把被借用的变量移入闭包

2026-09-07 09:04:37作者:何将鹤

导读

本文围绕 rustc 官方错误码文档 E0504.md 展开,系统讲解 Rust 编译器中一类经典的借用与移动冲突:当某个值仍处于借用状态时,试图通过 move 闭包把它"搬"进闭包。你会了解到这一错误的成因、四种典型修复思路(改借引用、收窄借用作用域、用 Arc 共享所有权等),以及它在现代 rustc 中"不再由编译器签发"的历史地位与底层实现依据。

历史地位:一个已停用但仍保留的错误码

E0504.md 开篇的第一句说明已经写得非常明确:

Note: this error code is no longer emitted by the compiler.

当前版本的 rustc 编译器已经不再签发 E0504。这一点可以从编译器源码中得到印证:

  • 在编译器各 crate(如 compiler/rustc_borrowckcompiler/rustc_mir_buildcompiler/rustc_typeck 等)的源码中均检索不到任何抛出 E0504 的诊断结构;
  • 但 E0504 仍然保留在官方注册表中。在 rustc_error_codes/src/lib.rserror_codes! 宏列表中可以看到 0500~0505 连续注册,其中就包含 0504, 这一项。

为什么"停用"却"保留注册"?lib.rs 的维护说明给出了答案:

Do not remove entries from this list. Instead, just add a note to the corresponding markdown file saying that this error is not emitted by the compiler any more, and remove all code examples that do not build any more by marking them with ignore (no longer emitted).

也就是说,历史错误码的编号与解释文档会被永久保留(防止既有错误码索引失效、链接 404),只是在其 Markdown 文档开头加上"不再签发"的说明。因此,学习 E0504 的价值在于:理解它背后那条至今依然生效的 Rust 所有权/借用规则,以及当代码里出现同类冲突时应该采用的修复范式。

错误成因:向闭包移动一个仍被借用的值

E0504 触发的情形是:一个被借用(borrow)的变量被尝试 move 进闭包。其原理在于 move 闭包会把捕获的变量按值转移所有权,而所有权转移(移动)会把原变量"掏空",这会使此前建立在它之上的借用失效——编译器必须拒绝这种会让悬垂引用出现的行为。

错误文档给出了如下的反例代码:

struct FancyNum {
    num: u8,
}

fn main() {
    let fancy_num = FancyNum { num: 5 };
    let fancy_ref = &fancy_num;

    let x = move || {
        println!("child function: {}", fancy_num.num);
        // error: cannot move `fancy_num` into closure because it is borrowed
    };

    x();
    println!("main function: {}", fancy_ref.num);
}

这段代码里存在两个相互冲突的意图:

  1. let fancy_ref = &fancy_num; 建立了对 fancy_num 的共享借用;
  2. let x = move || { ... fancy_num.num ... }; 由于是 move 闭包且按值读取 fancy_num.num,需要把 fancy_num 整个移入闭包
  3. 移动之后,main 末尾仍要使用 fancy_ref(它指向已移动的值),借用的前提被破坏。

正如文档总结的:"在值仍被借用期间,不存在任何办法把它移入闭包,因为那会使借用失效。"

理解这一点需要区分闭包捕获的两种粒度:

  • 借用捕获(by reference):闭包只是持有 &T,所有权仍留在原处,不会产生移动冲突;
  • 按值捕获 / 移动捕获(by value / move:闭包要求对被捕获路径的所有权,等价于一次 move,原位置随后不可再被访问。

move 关键字强制闭包"尽量按值捕获",当按值捕获的目标同时被外部借用时,就撞上了 E0504 所描述的约束。

修复思路一:闭包内改用引用捕获

如果闭包的生命周期不会超过被捕获的值,那就不必移动,改为让闭包捕获引用即可——引用只是"借用",不会把 fancy_num 的所有权搬走:

struct FancyNum {
    num: u8,
}

fn main() {
    let fancy_num = FancyNum { num: 5 };
    let fancy_ref = &fancy_num;

    let x = move || {
        // fancy_ref is usable here because it doesn't move `fancy_num`
        println!("child function: {}", fancy_ref.num);
    };

    x();

    println!("main function: {}", fancy_num.num);
}

注意这里的微妙之处:move 闭包捕获的是 fancy_ref 这个引用本身(一个 Copy 的指针值),移动它不会影响 fancy_num 本体。因此 fancy_num 仍留在原处,主函数之后依旧可以直接使用它。

选引用的前提是:闭包必须保证不会活得比 fancy_num 更久,否则你会得到"借用逃逸闭包"一类的错误——这类问题正是 borrowck 源码中 borrowed_data_escapes_closure 诊断所覆盖的场景(见 rustc_borrowck/src/diagnostics/conflict_errors.rsregion_errors.rs)。

修复思路二:用作用域块提前结束借用

如果值确实需要被移入闭包,那就必须先保证:在移动发生时,已经不存在任何指向它的引用。最直接的办法是用 {} 作用域把借用"圈"起来,让借用在使用完毕后立刻结束:

struct FancyNum {
    num: u8,
}

fn main() {
    let fancy_num = FancyNum { num: 5 };

    {
        let fancy_ref = &fancy_num;
        println!("main function: {}", fancy_ref.num);
        // `fancy_ref` goes out of scope here
    }

    let x = move || {
        // `fancy_num` can be moved now (no more references exist)
        println!("child function: {}", fancy_num.num);
    };

    x();
}

这里的关键机制是借用(借用检查器称之为 NLL,Non-Lexical Lifetimes)的活跃区间fancy_ref 在离开 {} 块后就不再被使用,其借用随即结束。此时 fancy_num 处于"无任何存活引用"状态,move 闭包再对它做所有权转移便完全合法。

这种"先借后移、缩短借用生命周期"的模式与相邻错误码 E0505 的文档建议完全一致。对照 E0505.md,它给出的修复选项同样包括:"避免移动变量""在移动前释放借用""为类型实现 Copy"。E0504 与 E0505 共同构成了同一物理约束(不能移走一个被借用的值)在不同代码形态下的两个历史诊断视角。

修复思路三:跨线程场景改用 Arc 共享所有权

当闭包要被送去别的线程执行(如 thread::spawn),引用捕获通常行不通,因为线程闭包要求捕获内容拥有 'static 生命周期,局部值的借用无法满足。此时正确的做法是放弃"移动 + 借用"的对抗,改用 Arc<T> 引用计数所有权:把值包进引用计数智能指针,闭包和主线程各持有一份克隆,谁都不需要"抢"原变量:

use std::sync::Arc;
use std::thread;

struct FancyNum {
    num: u8,
}

fn main() {
    let fancy_ref1 = Arc::new(FancyNum { num: 5 });
    let fancy_ref2 = fancy_ref1.clone();

    let x = thread::spawn(move || {
        // `fancy_ref1` can be moved and has a `'static` lifetime
        println!("child thread: {}", fancy_ref1.num);
    });

    x.join().expect("child thread should finish");
    println!("main thread: {}", fancy_ref2.num);
}

要点拆解:

  • Arc::newFancyNum 放到堆上,fancy_ref1 只是指向堆数据的智能指针
  • fancy_ref1.clone() 只递增引用计数并复制指针,并不复制底层数据;
  • 两个 move(一个进 thread::spawn 的闭包,一个留在主线程)移动的是各自的 Arc 句柄,Arc 本身是 'static 的,且底层值在引用计数归零前不会被释放;
  • 需要可变访问时,可搭配 Mutex<T>/RwLock<T> 使用(本文涉及的场景为只读,故仅需 Arc)。

如果你的数据跨线程且确实需要可变性Arc<Mutex<T>> 是同一思路的自然延伸——文档中的 Arc 示例是这条路径的起点。

底层机制:闭包捕获分析如何决定能否移动

要真正吃透 E0504,需要理解编译器是如何决定"闭包要捕获什么、以什么方式捕获"的。在 rustc 中,这一分析发生在借用检查阶段,核心数据结构是 closure captures(闭包捕获列表)

  • rustc_borrowck/src/type_check/mod.rs 中,类型检查会通过 tcx.closure_captures(def) 查询某个闭包实际捕获的路径(place);
  • rustc_borrowck/src/lib.rs 附近,借用检查同样读取 tcx.closure_captures(def),把捕获信息纳入 MIR 的借用与移动分析;
  • 捕获会区分借用捕获按值捕获:只有那些被闭包"按值 + 不可复制"捕获的路径才需要真正的所有权转移,也才会与外部存活借用发生冲突。

结合这一实现可以推断:rustc 对 E0504 这类错误的检测逻辑本质上属于"移动(move)与借用(borrow)冲突"分析——borrowck 需要同时掌握"该值此刻是否存在存活借用"与"闭包是否要求该值的所有权"两条信息,两者相交才产生冲突。这也是为什么该诊断逻辑位于 compiler/rustc_borrowck crate 而非语法解析或类型检查阶段:它依赖 MIR 构建完成后的借用集(BorrowSet)与移动数据分析。

从源码结构看,E0504 之所以整体退役,与 rustc 闭包捕获机制的持续精细化有关:后续版本在按值捕获与借用捕获之间引入了更细粒度的路径级判定,并把"捕获变量被移动"之类的场景并入了同族的其他诊断。例如在 rustc_borrowck/src/diagnostics/move_errors.rs 中可以看到现役错误码 E0507 的注释形态——error[E0507]: cannot move out of 'var', a captured variable in an 'FnMut' closure,它处理的就是"从闭包捕获变量中移出"这一相近问题。E0504 的场景如今会被同族、更精确的借用/移动冲突诊断所覆盖。

相邻错误码家族:E0500 ~ E0512

error_codes 文档目录 中,E0500~E0512 是一组彼此关联的借用检查(borrowck)错误码文档,共同描述了 Rust 借用规则的各个侧面。典型成员包括:

  • E0505:值在被借用期间被移出——与 E0504 物理约束相同、但移动场景更一般化,文档建议"避免移动 / 释放借用后再移动 / 实现 Copy";
  • E0507:不能从被借用处移出(包含"从被可变借用结构体中移出成员""从 FnMut 闭包捕获变量中移出"等具体形态);
  • E0502:同一时刻既不可变借用又可变借用等冲突借用情形;
  • E0501 / E0503 / E0506 / E0508 / E0509 等:覆盖借用冲突、竞态变体、重复移出、drop 后使用等各类移动/借用错误。

这些文档与 E0504 使用完全相同的模板(反例 + 成因 + 修复),且均为 rustc 官方错误码索引(error index)的组成部分。当你在现代工具链上遇到形似 E0504 的报错时,实际弹出的很可能是上述现役错误码,它们的修复范式与本篇介绍的四条思路完全互通。

结语与查阅指引

总结本篇文章的核心结论:

  1. E0504 是一个历史错误码,描述"试图把被借用的变量移入 move 闭包"这一冲突;当前编译器已不再签发它,但其文档与编号被永久保留在官方注册表中;
  2. 该错误的本质是移动会作废既有借用,因此修复必须在"闭包不移动值"(改借引用)与"移动时已无借用"(收窄作用域 / 改用 Arc 共享)之间二选一;
  3. 在 rustc 中,这类判断由 rustc_borrowck 基于 MIR 的闭包捕获分析(closure_captures)与借用集完成,底层逻辑至今仍作用于同族现役错误码。

想要在源码层面继续深入,可以依次阅读:

当你的代码再次遭遇"不能把借用的值放进 move 闭包"的困惑时,请记住这篇文章里的判断顺序:闭包能否活得比值短?能,就捕获引用;值必须走,就先让借用结束;要跨线程,就上 Arc 三选一,冲突自解。

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

项目优选

收起
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.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390