首页
/ Rust 编译器 E0029 深度解析:范围模式为何只接受数字与字符

Rust 编译器 E0029 深度解析:范围模式为何只接受数字与字符

2026-09-06 11:54:01作者:幸俭卉

match 表达式中的范围模式(如 "a" ..= "z"1..=10)是 Rust 模式匹配中一个常见的陷阱来源。本文以 Rust 编译器官方错误代码文档 E0029 为主线,完整解读"为什么 match 中的范围只允许数字和字符"这一错误的成因、rustc 源码中的判定实现位置,以及使用 guard 的正确替代方案,帮助读者在遇到 only char and numeric types are allowed in range patterns 报错时快速定位问题并正确改写代码。

错误现象:用字符串做范围匹配

官方文档给出的标准错误示例如下(可直接复现):

let string = "salutations !";

// 字符串的有序关系无法在编译期求值,因此这样写不行:
match string {
    "hello" ..= "world" => {}
    _ => {}
}

// 更通用的等价写法是使用 guard:
match string {
    s if s >= "hello" && s <= "world" => {}
    _ => {}
}

在真实仓库中,对应的官方 UI 测试 E0029.rs 使用了一个更精简的复现:

fn main() {
    let s = "hoho";

    match s {
        "hello" ..= "world" => {}
        _ => {}
    }
}

编译器输出的完整诊断(见 E0029.stderrE0029-teach.stderr)如下:

error[E0029]: only `char` and numeric types are allowed in range patterns
  --> E0029-teach.rs:7:9
   |
LL |         "hello" ..= "world" => {}
   |         -------^^^^^-------
   |         |           |
   |         |           this is of type `&'static str` but it should be `char` or numeric
   |         this is of type `&'static str` but it should be `char` or numeric
   |
   = note: In a match expression, only numbers and characters can be matched against a range. ...
   For more information about this error, try `rustc --explain E0029`.

两个要点值得注意:

  • 错误信息精确指出"端点类型应为 char 或数字类型",并逐个标注每个端点的实际类型(这里是 &'static str);
  • 末尾附带的 --explain 提示会把开发者引向 E0029 的完整说明文档(即本文所依据的 E0029.md)。

报错原因:编译期必须能判定范围的合法性

E0029.md 给出的核心解释只有一句话,但信息量很大:

In a match expression, only numbers and characters can be matched against a range. This is because the compiler checks that the range is non-empty at compile-time, and is unable to evaluate arbitrary comparison functions.

拆开来看有两层原因:

  1. 编译器要在编译期验证范围非空。 范围模式 lo .. hi(或半开区间 lo .... hi)在 lowering 阶段会被常量求值检查,必须能证明 lo 不大于 hi(否则该分支是空的、永不命中)。这要求区间的两个端点在编译期就能确定大小关系;
  2. 编译器无法求值任意类型的比较函数。 数字与 char 的大小比较是内置的、可常量求值的;而 &strString、枚举、结构体等类型的 Ord 实现是普通方法调用,其结果依赖运行期数据,编译期无法求值,因此不能作为范围模式的端点。

从源码结构看,这一限制被落实在模式类型检查阶段,而不是解析阶段——也就是说 "hello" ..= "world" 这种写法在语法上完全合法,直到类型检查才会被判为错误。

源码级实现:rustc_hir_typeck 中的判定链

E0029 在编译器中的唯一触发点位于 rustc_hir_typeck/src/pat.rs,整个判定可以分为三步。

第一步:端点类型预判(calc_side)

check_pat_range 首先对范围的两个端点分别求类型,并提前做一次"快速失败"检查:

// compiler/rustc_hir_typeck/src/pat.rs (L1022-L1039 节选)
let calc_side = |opt_expr: Option<&'tcx hir::PatExpr<'tcx>>| match opt_expr {
    None => None,
    Some(expr) => {
        let ty = self.check_pat_expr_unadjusted(expr);
        let ty = self.resolve_vars_with_obligations(ty);
        let fail =
            !(ty.is_numeric() || ty.is_char() || ty.is_ty_var() || ty.references_error());
        Some((fail, ty, expr.span))
    }
};
let mut lhs = calc_side(lhs);
let mut rhs = calc_side(rhs);

源码注释明确说明:这次提前检查"不为正确性,而为更好的诊断"——如果不早在这里拦截,当 expected 被剥离引用而端点类型仍为 &str 时,会先抛出一个更令人困惑的类型统一错误。

第二步:与期望类型统一后的最终校验

若两端都通过了预判(例如端点都是类型推断变量),函数会继续把端点类型与 match 目标的期望类型统一,再对统一后的类型做同样的 is_numeric() || is_char() 校验(见 pat.rs L1068-L1082)。这一步处理"两端都是推断变量"的边角情况,避免误报"_ 不是 char 或数字"这类噪音错误。

第三步:emit_err_pat_range 发出 E0029

真正发出错误的是 emit_err_pat_range

let mut err = struct_span_code_err!(
    self.dcx(),
    span,
    E0029,
    "only `char` and numeric types are allowed in range patterns"
);

该函数还会:

  • 为失败的一侧(或两侧)分别打上 this is of type &'static strbut it should bechar` or numeric`` 的标注,这正是前面诊断输出中的双行提示;
  • self.tcx.sess.teach(err.code.unwrap()) 为真时(即 pat.rs L1133-L1141),追加 E0029 的"教学笔记"——这条 note 的文字与 E0029.md 结尾的说明完全一致,说明两者由同一套错误代码文档体系维护;
  • 若端点类型本身带有推断错误,则把 E0029 降级为 delayed bug,避免连锁报错干扰主要问题定位。

rustc --explain E0029 是怎么找到 E0029.md 的

读者常有的一个疑问是:命令行 rustc --explain E0029 输出的长说明从哪里来?答案就在本仓库的 rustc_error_codes/src/lib.rs

//! This library is used to gather all error codes into one place, to make
//! their maintenance easier.
...
#[macro_export]
#[rustfmt::skip]
macro_rules! error_codes {
    ($macro:path) => (
        $macro!(
        ...
        0029,
        ...
        );
    )
}

该 crate 的文件头注释说明:error_codes/EXXXX.md 是各错误代码的说明文件,error_codes! 宏是所有在用错误代码的单一登记表(E0029 注册于 lib.rs 第 44 行)。rustc_errors/src/codes.rs 通过展开该宏生成错误码常量与诊断表的映射关系,于是 --explain E0029 与诊断末尾的 teach note 都能追溯到 E0029.md 这份文档。这也意味着:如果你在编译器仓库中修正错误说明,只需修改对应的 E00XX.md,测试快照(.stderr)与之共同约束输出一致性。

正确的替代方案

结合文档建议与源码判定逻辑,处理 E0029 的通用思路有三条:

1. 可排序类型用 guard(官方推荐)

对于字符串、枚举等实现了 Ord 的类型,用带绑定变量的 guard 表达区间:

match string {
    s if s >= "hello" && s <= "world" => {
        // 命中区间
    }
    _ => {}
}

guard 在运行期求值,不受"编译期必须能判定"的限制,语义与闭区间 ..= 一致。若只需单侧边界,可写成 s if s >= "hello" 对应半开区间的效果。

2. 确实属于数字/字符时,检查端点类型

范围模式支持的类型集合以 ty.is_numeric() || ty.is_char() 为准(见 pat.rs L1073),即所有整数类型、浮点类型和 char。注意两个易错点:

  • char 范围是合法的:'a' ..= 'z' 可以正常匹配,无需 guard;
  • 端点必须是编译期常量表达式(字面量、常量、const 函数调用等),运行时变量做端点同样无法通过编译期检查。

3. 枚举值改用离散模式列举

枚举没有自然数值序,范围模式本就无意义。正确做法是用 | 列举各变体:

match level {
    Level::Low | Level::Medium => {}
    Level::High => {}
}

如何用仓库测试验证你的修复

本仓库将 E0029 的诊断行为固化为 UI 测试,可作为改写代码后的自检标准:

从源码结构看,其他涉及范围模式类型错误的诊断(如 half-open-range-patternsmatch-range-fail 等测试)也都经由 check_pat_range 这条路径发出,因此理解本文所述的三步判定链,可以覆盖所有"范围模式 + 非法端点类型"类的编译错误排查。

小结

  • E0029 的本质:match 范围模式要求编译期即可判定区间合法(非空),而数字与 char 的比较是唯一可在常量求值中完成的大小关系,故端点类型被限定为数字或字符;
  • 报错点唯一:rustc_hir_typeck/src/pat.rscheck_pat_rangeemit_err_pat_range,诊断文案与 E0029.mdrustc_error_codeserror_codes! 登记表统一管理;
  • 修复路径:可排序类型用 guard(s if s >= lo && s <= hi),数字/字符场景核对端点是否常量且类型正确,枚举改用 | 离散列举。
登录后查看全文
热门项目推荐
相关项目推荐