Rust 编译错误 E0595 全解:闭包修改不可变捕获变量的历史与去向(已在 rustc 中退役并迁移至 E0594)
导读
在 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 的完整正文非常精炼,可拆解为三点:
- 状态声明:该错误码已不再由编译器发出(no longer emitted);
- 历史语义:闭包不能修改被捕获的不可变变量(Closures cannot mutate immutable captured variables);
- 错误示例与修复:
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.rs 与 ty/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,产出 E0596cannot 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.rs 与 E0594.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.rs 中 cannot_reassign_immutable |
| E0506 | 赋值给已被借用(borrowed)的位置 | 借用未结束时对被借值赋值 | 仍在使用,见 cannot_assign_to_borrowed |
粗看它们都是“赋值出错”,但边界清晰:E0384 针对在同一作用域内第二次赋值(与是否经由闭包无关);E0506 针对存在活跃借用;E0594 针对位置本身不可变(含通过闭包捕获引用到不可变绑定);E0596 则负责可变借用那一支。
在测试与文档体系中的佐证
作为“已退役”状态的可验证证据:
- 在 tests/ui/error-codes/ 目录中只存在
E0594.rs、E0596.rs、E0597.rs、E0599.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_borrowck 的 cannot_assign 诊断构造器发出,处理入口在 mutability_errors.rs 的 report_mutability_error)。遇到此类问题时,正确的修复是让被闭包写入的变量以 mut 声明;若需要的是可变借用,则留意 E0596。理解这段演进,能帮助你在面对新版编译器输出与旧文档不一致时快速对齐语义,也更清楚闭包“捕获即引用、可变性归属绑定”的 Rust 设计原则。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00