首页
/ rustc 错误码 E0302 解析:为何匹配守卫(match guard)中禁止赋值,以及该错误在现代编译器中的归宿

rustc 错误码 E0302 解析:为何匹配守卫(match guard)中禁止赋值,以及该错误在现代编译器中的归宿

2026-09-07 19:59:46作者:乔或婵

在 Rust 编译器(rustc)的演进过程中,不少早期错误码被合并、拆分或彻底移除。E0302 正是这样一个"已经退役"的错误码:它曾经用来禁止在 match 的模式守卫(pattern guard)中执行赋值操作,原因是匹配过程不允许引入副作用。本指南以 E0302.md 为主体,结合 rustc 源码中守卫(guard)的下层构建逻辑、借用检查器的报错路径以及错误码注册机制,讲清三件事:E0302 当年要表达什么规则、这条规则为什么至今依然成立、以及今天违反该规则时编译器会如何报错与如何修复。读完你会理解 match guard 必须"纯计算"的设计动机,也能在源码中快速定位 E0594 等现代错误码的产生位置。

E0302 的历史定位:一条"已不再由编译器发出"的错误码

打开 E0302.md,文档第一行就是一句醒目的说明:

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

也就是说,E0302 已经是历史错误码,当前版本的 rustc 不再产生该编号的诊断。但在 rustc 源码中,它并没有被简单删除。查看 lib.rs,顶部注释对"退役错误码"的处理有明确规定:绝不从错误码清单中删除条目,只需在对应的 Markdown 文档顶部加注"该错误已不再由编译器发出",并删除那些已经无法编译通过的示例即可。

这条规则在 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 (see E0001.md for an example), and remove all code examples that do not build any more by marking them with ignore (no longer emitted).

因此 E0302 至今仍保留在 error_codes! 宏展开的编号列表里(可定位到 lib.rs 处的 0302 项)。这样做的目的很明确:

  1. 编号永不回收:已经被官方文档化的错误码如果被重新赋予新含义,会误导通过 rustc --explain E0302 检索历史的开发者;
  2. 历史文档持续可查:说明文档随编译器一同发布,即便错误不再触发,开发者仍能理解其背后的语言设计约束;
  3. tidy 工具链可自动校验rustc_error_codes 库注释提到,宏内容与各 EXXXX.md 文档的一致性由 tidy 的 check_error_codes_docs 检查(见 lib.rs),文档与注册表必须保持同步。

从错误码生命周期管理的角度,E0302 是理解 rustc"如何优雅退役一个错误码"的典型样例。

规则核心:为什么匹配守卫中不允许赋值

E0302 当初针对的是这样一段代码——在 match 的分支守卫里对匹配对象或相关环境变量做赋值:

match Some(()) {
    None => { },
    option if { option = None; false } => { },
    Some(_) => { } // When the previous match failed, the option became `None`.
}

文档给出了两条论证理由,都与"模式匹配必须是无副作用的穷尽检查"这一设计基石相关:

  • 匹配不能引入副作用(matching cannot have side effects)。match 的分支选择应当是"看一个值"的纯判定过程。守卫(guard)本质是分支条件的一部分,若在其中赋值,就会改写被匹配的值或外部环境;
  • 副作用会破坏穷尽性(exhaustiveness)保证。Rust 编译期会对 match 做穷尽性检查,判断所有可能的值是否都有分支覆盖。上述示例恰好是一个反证:若守卫中允许 option = None,那么当该守卫执行并返回 false 时,option 已被改写成 None,后续本应覆盖剩余情形的 Some(_) 分支将永远不可能命中——整个 match 的穷尽性结论建立在"匹配过程中值不变化"的前提之上,而赋值让这个前提崩塌。

换言之,守卫表达式应当被视为"只读校验":它可以引用被匹配的值与作用域内变量,但不得修改它们,否则匹配结果将依赖于求值顺序,语义上进入未定义区域。

值得注意的是,这一约束的历史不止 E0302 一处。同目录的 E0510.md 曾记录"在 match guard 中为匹配值赋值"的另一类问题(该错误同样退役),可见 rustc 早期曾尝试以多个错误码细化"守卫中的非法变更"场景;而 E0302 聚焦的是其中"对可空绑定 / 被匹配变量做赋值"这一类。

现代编译器的报错形态:E0302 → E0594 的演进

虽然 E0302 编号已不再被使用,但它所禁止的代码今天依然无法通过编译——只是错误报告改由借用检查器(borrowck)以 E0594("cannot assign to ...",即对不可变值的赋值)的形式发出。

一个直接证据是 E0302 文档中代码块的属性标注:

```compile_fail,E0594

compile_fail,E0594 是 compiletest 体系中的测试标记,表示该代码块在测试中应当编译失败并产生 E0594。也就是说,这份"退役"文档本身仍被复用为 E0594 的一个端到端测试样例——示例中的 option = None 在现代 rustc 下会报 "cannot assign to immutable local variable option" 之类的 E0594 错误。

追溯源码可以确认 E0594 的确切产生路径:

  • 借用检查器在 borrowck_errors.rs 中提供了统一的诊断构造入口 cannot_assign,其核心即 struct_span_code_err!(self.dcx(), span, E0594, "cannot assign to {}", desc)
  • 当检查到对不可变位置的可变访问时,mutability_errors.rsreport_mutability_error 会按访问类型分派:对于 AccessKind::Mutate(写入/赋值)直接调用 self.cannot_assign(...),从而产出 E0594。

因此,曾经对应 E0302 的"守卫里赋值"场景,如今和普通"给不可变变量赋值"共享同一条 E0594 报告通道。关于 E0594 的完整语义与修复示例,可参考 E0594.md:它将不可变结构体字段赋值视为典型错误场景,修复方式是给绑定加上 mut

补充:与 E0302/E0594 类似的退役码还有 E0595(闭包内修改不可变捕获变量,已不再单独发出,见 E0595.md),其示例同样以 compile_fail,E0594 标注。可以看出 rustc 正把这些"不同语法位置上的同类可变性违规"逐步收敛到少数几个核心错误码上。

底层机制:守卫期间模式绑定被降级为不可变引用

从 E0302 的禁令到 E0594 的报错,中间隔着一个关键的下层设计——匹配守卫在执行期间,模式绑定的变量一律按"只读引用"处理。这在 MIR 构建阶段就已固化。

在 match 分支的 MIR 生成代码 matches/mod.rs 中,可以读到如下逻辑注释与实现:

For each pattern ident P of type T, ref_for_guard is a reference R: &T pointing to the location matched by the pattern, and every occurrence of P within a guard denotes *R.

也就是说:当某个模式标识符 P 需要在守卫中被使用时,编译器会为它创建一个指向被匹配位置的共享引用(Rvalue::Ref(..., BorrowKind::Shared, ...)),守卫中对 P 的一切访问都经过该引用间接进行。对于按值绑定(ByRef::No)的绑定,守卫期间取的是共享引用,天然不可写;只有真正进入分支体(OutsideGuard)之后,才按绑定的原始模式模式恢复其应有的可变性(见 matches/mod.rs 中为守卫外路径单独创建值绑定的逻辑)。

这一设计与借用检查器侧的处理遥相呼应:

  • mutability_errors.rs 中针对"该局部变量是守卫临时引用(is_ref_for_guard)"的情形,专门生成了原因描述 "as it is immutable for the pattern guard"(它对于模式守卫而言是不可变的);
  • 类似的提示还出现在该文件其他地方,例如 "variables bound in patterns are immutable until the end of the pattern guard"(模式中绑定的变量在模式守卫结束前不可变),以及 place_ext.rs 对守卫场景的可变借用跟踪注释。

把三段证据拼起来,"守卫里不能赋值"就不再只是一条错误码文档里的口号,而是有明确实现支撑的编译期约束:

  1. MIR 构建期把守卫中的模式绑定做成只读引用(RefWithinGuard);
  2. 任何对该引用解引用后的写入都会触发借用检查器关于不可变位置违规的判定;
  3. 借用检查器以 E0594(历史上为 E0302)报告,并提示该位置"对模式守卫不可变"。

如何复现、修复与在仓库中验证

复现现代报错(E0594)

将 E0302 文档中的示例保存为本地文件后用 rustc 编译(或放入 compiletest 的 ui 测试目录),即可观察到 E0594 输出:

fn main() {
    match Some(()) {
        None => { }
        option if { option = None; false } => { }
        Some(_) => { }
    }
}

编译器会在 option = None 处指出 cannot assign to immutable local variable option(E0594)。这就是 E0302 所禁止行为的现代报错形态。

正确的修复姿势

把守卫设计成纯只读判定,把需要产生"副作用"的逻辑挪到分支体内执行。对 E0302 示例而言,守卫中的 option = None 本质是想在分支内改变变量状态,正确的做法是去掉赋值,让分支体完成状态变更:

match Some(()) {
    None => { }
    option if /* 只做只读判定的条件表达式 */ => {
        // 需要修改 option 时在此进行
    }
    Some(_) => { }
}

如果确实需要同时做"判断 + 变更",应将其拆成独立步骤,例如先在 match 前用普通的 if/let 逻辑处理状态,再对处理后的值执行穷尽匹配。守卫应始终保持在"引用外部变量而不修改它们"的范畴内,这与 Rust 的整体可变性/别名规则一致。

在仓库中快速定位关联实现

若要进一步深入,推荐按下列路径在本仓库(rustc 源码)中检索:

关注点 文件 定位线索
E0302 历史说明与退役标注 E0302.md 全文
错误码注册表(含退役码) lib.rs 第 176 行附近的 0302;第 21-24 行退役处理规则
E0594 语义与修复示例 E0594.md 全文
E0594 诊断构造 borrowck_errors.rs cannot_assign 函数
不可变位置违规的报告分派 mutability_errors.rs AccessKind::Mutate 分支
守卫临时引用(RefWithinGuard)的 MIR 构建 matches/mod.rs bind_matched_candidate_for_guard 的共享引用生成

另外,E0302 文档中那段代码块自带的 compile_fail,E0594 属性本身即是仓库中可运行的测试证据:rustc 的 ui/compiletest 测试体系会根据该属性断言"该代码必须产生 E0594 错误",从而把这条历史规则持续锁在测试矩阵中,防止回归。

小结

E0302 虽然已退出历史舞台,但它承载的规则——匹配守卫必须是只读、无副作用的环境——在今天的 rustc 中依旧被严格执行,只是报错改由 E0594 承担。从错误码注册表对退役码"保留编号、保留文档、同步到测试"的处理方式,到 MIR 构建期将守卫绑定降级为共享引用的实现,再到借用检查器输出专门面向守卫场景的可变性问题提示,一条小小的错误码背后贯穿了 rustc 在匹配语义、借用检查与诊断管理三套子系统中的一致设计。理解 E0302,本质上是理解了 Rust 匹配系统的一条核心不变量:匹配是查看值,而不是修改值。

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