首页
/ Rust 编译错误 E0595 全解:闭包修改不可变捕获变量的历史与去向(已在 rustc 中退役并迁移至 E0594)

Rust 编译错误 E0595 全解:闭包修改不可变捕获变量的历史与去向(已在 rustc 中退役并迁移至 E0594)

2026-09-08 19:39:14作者:乔或婵

导读

在 Rust 编译器的错误码文档体系(compiler/rustc_error_codes/src/error_codes/)中,每个错误码都有一个独立文档条目,E0595 正是其中之一。它记录了一个极具代表性的借用检查错误:闭包(closure)试图修改被捕获(capture)的不可变变量。需要注意的是,这篇文档第一行就声明 this error code is no longer emitted by the compiler——E0595 是一个已经退役的错误码,如今同样的代码会改由 E0594 报告。本文将以该文档为线索,厘清 E0595 的原始语义、退役后的实际报告路径(rustc_borrowck 的 mutability 诊断体系)、对应修复方法,以及它与 E0594/E0596/E0384 等邻近错误码的边界,帮助你准确理解闭包捕获与变量可变性之间的编译期约束。

E0595 条目本身说了什么

E0595.md 的完整正文非常精炼,可拆解为三点:

  1. 状态声明:该错误码已不再由编译器发出(no longer emitted);
  2. 历史语义:闭包不能修改被捕获的不可变变量(Closures cannot mutate immutable captured variables);
  3. 错误示例与修复
let x = 3; // 错误:closure cannot assign to immutable local variable `x`
let mut c = || { x += 1 };

修复方式是让被捕获的变量绑定可变:

let mut x = 3; // ok!
let mut c = || { x += 1 };

注意示例块上的属性是 compile_fail,E0594 而非 E0595,这正是“E0595 已退役、场景移交给 E0594”的直接体现——文档作者在保留历史讲解的同时,把可执行示例的期望错误码指向了当前实际报告方。

为什么 E0595 退役:它被并入了 E0594

搜索整个 compiler/ 目录可以发现:E0595 除了本文档自身之外没有任何源码引用;而 E0594 仍然活跃于借用检查器(borrowck)的诊断代码中,说明 E0595 对应的场景后来统一收编进 E0594 的消息体系。

历史上的 E0595

E0595 时代,下面的代码会触发 E0595: closure cannot assign to immutable local variable x

let x = 3;
let mut c = || { x += 1 }; // 闭包捕获 x,但绑定不可变

捕获(capture)≠ 可变性(mutability)|| { x += 1 } 中的闭包体按引用捕获了 x,对 x 执行 += 1 属于写入操作。编译器要求在写入时该内存位置的“所有者绑定”是可变的——这正是借用检查器(MIR borrow check)中 mutability 检查的职责。既然 let x 没有 mut,就构成对不可变位置的赋值错误。

现在的 E0594 接管

borrowck_errors.rs 中,borrow check 的 mutability 错误统一通过一个构造器生成:

pub(crate) fn cannot_assign(&self, span: Span, desc: &str) -> Diag<'diag> {
    struct_span_code_err!(self.dcx(), span, E0594, "cannot assign to {}", desc)
}

也就是说,如今凡是“给不可变位置赋值”的非法操作,都会被报告为 E0594 cannot assign to ...——包括向被闭包捕获的不可变变量赋值这一历史 E0595 场景。这解释了为什么 E0595.md 中错误示例的注解是 compile_fail,E0594

另外两处 rustc 内部文档中的示例也印证了这一点:mir/syntax.rsty/closure.rs 中讲解闭包捕获/不可变赋值语义时使用的可执行示例,其期望错误码均为 compile_fail,E0594,与本文档的迁移说明一致。

底层实现:mutability 诊断如何走到 E0594

E0595 对应语义在编译器中的“继承者”,本质上是 borrowck 对 MIR 中不可变 place 的赋值检查。其处理入口位于 mutability_errors.rs,核心函数是 report_mutability_error

从源码结构可以还原如下链路:

  • borrowck 检测到对某个 PlaceRef 的写入或可变借用不合法时,按 AccessKind 分支处理:
    • AccessKind::Mutate(纯赋值场景):调用上面的 cannot_assign,产出 E0594,并在分支中把动词抽象为 assign / written to
    • AccessKind::MutableBorrow(可变借用场景):调用 cannot_borrow_path_as_mutable_because,产出 E0596 cannot borrow {} as mutable
  • 当不可变原因与被闭包捕获的局部变量相关时,诊断还会尝试给出建议性标注(span_suggestion / span_label),例如提示“consider changing this to be mutable”,即建议在绑定处补上 mut

一个值得注意的实现细节:当同一不可变绑定被多次尝试可变借用时(例如一个不可变 let 绑定被多个 &mut 请求命中),borrowck 会做错误合并——get_buffered_mut_error 会把第二次及以后的尝试合并进以绑定声明位置为主 span 的单个诊断,并标注 not mutable,避免刷屏(见 mutability_errors.rs)。这体现了 rustc 在“同一个根因(绑定缺少 mut)下收敛诊断数量”的设计思路。

正确修复:变量可变性归位

回到文档中的修复建议。核心原则是:谁拥有这份数据,谁就决定它是否可写

// 错误写法:x 不可变,却要被子闭包写入
let x = 3;
let mut c = || { x += 1 };
// E0594: cannot assign to `x`, as it is not declared as mutable
// 修复:让绑定可变
let mut x = 3;
let mut c = || { x += 1 };

补充说明:

  • 这里对 c 本身加不加 mut 只决定闭包变量是否可被重新赋值,不影响闭包体内部修改捕获变量的合法性;上述场景需要的是 x 可变;
  • 若闭包只是在读取 x,则 x 无需 mut,这也是闭包最常见、最安全的使用形态;
  • 可变借用(&mut)与直接赋值走的是不同诊断路径:前者对应 E0596,后者对应 E0594。

邻近错误码家族:避免概念混淆

E0595 退役之后,围绕“不可变性”的常用错误码容易混淆,这里结合仓库中的 error_codes 目录 与 borrowck 源码作一区分:

错误码 语义 触发形态 当前状态
E0594 cannot assign to ...,对不可变位置赋值 给未声明为 mut 的变量/字段/闭包捕获值赋值,如 ss.earth = 2 仍在使用,见 borrowck_errors.rsE0594.md
E0595 闭包修改不可变的被捕获变量 let x = 3; let c = || { x += 1 }; 已退役,场景移交 E0594
E0596 cannot borrow ... as mutable,对不可变位置取可变引用 &mut x 其中 x 未加 mut,包括经闭包捕获后取可变借用 仍在使用,见 borrowck_errors.rs
E0384 cannot assign ... twice to immutable variable / 对不可变参数重复赋值 同一不可变绑定被二次赋值 仍在使用,见 borrowck_errors.rscannot_reassign_immutable
E0506 赋值给已被借用(borrowed)的位置 借用未结束时对被借值赋值 仍在使用,见 cannot_assign_to_borrowed

粗看它们都是“赋值出错”,但边界清晰:E0384 针对在同一作用域内第二次赋值(与是否经由闭包无关);E0506 针对存在活跃借用;E0594 针对位置本身不可变(含通过闭包捕获引用到不可变绑定);E0596 则负责可变借用那一支。

在测试与文档体系中的佐证

作为“已退役”状态的可验证证据:

  • tests/ui/error-codes/ 目录中只存在 E0594.rsE0596.rsE0597.rsE0599.rs 等当前活跃错误码的用例,没有 E0595.rs,说明 rustc 测试套件已不再针对 E0595 单独断言;
  • compiler/ 源码树中 E0595 仅出现于本错误码文档自身以及 E0594.md 的可执行示例注解中,不存在任何 struct_span_code_err! 仍以 E0595 作为错误码的发出点。

rustc 在整理错误码时,会以“保留文档条目 + 标注退役”的方式处理那些被合并/重构掉的代码,而非直接删除条目——这正是 E0595.md 的存在意义:当你在历史资料、旧版本教程或存量代码评审中遇到 E0595 时,可以立刻定位到它今天的等价物 E0594,并理解闭包捕获不变量的修复路径(为被写入的捕获变量补 mut),不会因错误码改名而困惑。

小结

E0595 是 rustc 早期用于报告“闭包修改被捕获的不可变变量”的错误码,如今已不再独立发出,该场景统一由 E0594 cannot assign to ... 承担(经 rustc_borrowckcannot_assign 诊断构造器发出,处理入口在 mutability_errors.rsreport_mutability_error)。遇到此类问题时,正确的修复是让被闭包写入的变量以 mut 声明;若需要的是可变借用,则留意 E0596。理解这段演进,能帮助你在面对新版编译器输出与旧文档不一致时快速对齐语义,也更清楚闭包“捕获即引用、可变性归属绑定”的 Rust 设计原则。

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

项目优选

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