Rust 模式匹配中的前导竖线(Leading Vert)语法:从 RFC 1925 提案到 or-patterns 的落地

原创2026-10-14 22:38:551,622 阅读
文章标签:文档

Rust 模式匹配中的前导竖线(Leading Vert)语法:从 RFC 1925 提案到 or-patterns 的落地

本文以 Rust 官方 RFC 仓库中的 text/1925-optional-match-vert.md(Feature Name:match_vert_prefix)为核心,系统讲解"在 match 分支模式开头允许可选 | 前缀"这一语法提案的动机、语法设计与其最终落地路径:前导竖线后来通过 text/2535-or-patterns.md 中的 top_pat : '|'? pat<allow_top_alt> ; 规则正式进入 Rust 顶层模式语法。读完本文,你将理解前导 | 解决的代码对齐问题、它与其他 or-patterns 语法(text/2175-if-while-or-patterns.md)的关系,以及它在嵌套模式中为何被拒绝。

提案背景:让模式分支的每个 | 都能对齐

在 Rust 中,match 的每个分支可以用 | 连接多个备选模式,例如:

use E::*;

enum E { A, B, C, D }

// 这是当前合法的 Rust
match foo {
    A | B | C | D => (),
}

RFC 1925 提出的目标非常简单:允许在第一个备选模式之前也写一个 |,即允许前导竖线:

match foo {
    | A | B | C | D => (),
}

这一想法直接来自 F# 的使用体验。在 F# 中,联合类型(union,相当于 Rust 的枚举)的声明本身就采用"每个分支前都带 |"的风格,且以下三种写法完全等价:

// 普通联合类型
type IntOrBool = I of int | B of bool
// 为了风格一致,让每一行看起来都一样
type IntOrBool =
   | I of int
   | B of bool
// 允许折叠成一行
type IntOrBool = | I of int | B of bool

F# 的 match 语句同样延续这种风格——注意这里的每个 | 都是严格对齐的,而当前的 Rust 做不到这一点:

match foo with
    | I -> ""
    | B -> ""

RFC 作者在用过 F# 的这种风格后,认为 Rust 语言层面不支持前导 | 是一种限制,因此提出通过"对现有语法做直接扩展"来引入该能力。

核心动机:最大化 | 的对齐一致性

当前 Rust 对齐的尴尬

考虑一个多行排列的 or-pattern。在提案之前,Rust 只能做到以下两种"不一致"的写法之一:

use E::*;

enum E { A, B, C, D }

match foo {
//  |
//  V 这里不一致地缺少一个 `|`
      A
    | B
    | C
    | D => (),
}

match foo {
    A |
    B |
    C |
    D => (),
//    ^ 同样不一致:这是序列的最后一项,
//      由于后面没有更多项,省略后续的 `|` 也可以认为是合理的
}

第一种写法首行缺少 |,后续行却有 |;第二种写法把 | 放在行尾,最后一行又特殊。两种风格都无法让每个备选模式前的 | 整齐对齐。

提案允许的写法

RFC 1925 提议的语法允许这样写:

use E::*;

enum E { A, B, C, D }

match foo {
    | A
    | B
    | C
    | D => (),
//  ^ 通过配齐前导竖线获得了视觉一致性
}

每一行都以 | 开头,垂直方向完全对齐,这正是 F# 风格在 Rust 中的映射。

单行 match 的灵活性

前导 | 还给单行写法带来了风格自由。下面两种写法完全等价,不存在任何语义歧义,选择哪种只是风格偏好:

use E::*;

enum E { A, B, C, D }

// 带前导竖线
match foo {
    | A | B | C | D => (),
}

// 当前已允许的写法
match foo {
    A | B | C | D => (),
}

RFC 原文明确强调:这两种写法之间不应该有任何歧义,偏好应归结为风格选择。

多分支场景:所有等价写法一览

RFC 1925 给出了同一逻辑下四种风格等价的多分支 match,用于说明前导 | 不会改变任何语义,只影响排版:

use E::*;

enum E { A, B, C, D }

match foo {
    A | B => println!("Give me A | B!"),
    C | D => println!("Give me C | D!"),
}

match foo {
    | A | B => println!("Give me A | B!"),
    | C | D => println!("Give me C | D!"),
}

match foo {
    | A
    | B => println!("Give me A | B!"),
    | C
    | D => println!("Give me C | D!"),
}

match foo {
    A | B =>
        println!("Give me A | B!"),
    C | D =>
        println!("Give me C | D!"),
}

混合风格的边界情况

前导 | 允许与否是逐分支独立的。即使在一个分支前写 |、另一个分支前不写,也完全合法,只是视觉上略有不一致。RFC 用以下例子说明这一点:

use E::*;

enum E { A, B, C }

match foo {
    | A
    | B => {},
    | C => {}
//  ^ 按照上面的风格,每个分支的第一个元素前都可以放一个 `|`
}

match value {
    | A
    | B => {},
    C => {}
//  ^ 给 `A` 加了 `|` 却没给 `C` 加,看起来不太一致,
//    但这很难说非法——分支永远位于 `=>` 之后,
//    这不是 *语法* 层面需要过分关心的事
}

也就是说,RFC 作者认为这种对齐的不一致性属于风格问题,语法层面不需要禁止。

详细设计:语法上的一处最小改动

RFC 1925 的详细设计极其克制:只需要在 match 分支模式的语法产生式中,给第一个 pat 前加一个可选的 '|',语法中其余部分无需任何改动。

// 变更前
match_pat : pat [ '|' pat ] * [ "if" expr ] ? ;
// 变更后
match_pat : '|' ? pat [ '|' pat ] * [ "if" expr ] ? ;

其中 [ "if" expr ] ? 对应 match 分支的守卫条件(guard)。变更后语法仍保持上下文无关,解析器只需允许模式序列以单个前导 | 开头即可。RFC 作者坦言自己并不清楚具体实现细节,但明确断言除这一处产生式外,语法中的其他部分都不需要更新。

前导 | 在 if let、while let 等场景的延伸

虽然 RFC 1925 只针对 match 提出 match_pat 的改动,但这一"可选前导 |"的语法模式随后被后续 RFC 推广到了更多位置。例如 text/2175-if-while-or-patterns.md(RFC 2175)在为 if let、while let、let 与 for 引入顶层 or-patterns 时,其语法产生式同样包含了可选的 '|'?:

if_let_expr : "if" "let" '|'? pat [ '|' pat ] * '=' expr '{' block '}'
               else_tail ? ;

并指出这些构造可以通过语法降级(syntax lowering)转换成 match 与 loop + match 表达式。

落地验证:RFC 2535 如何正式引入前导 |

仓库中的 text/2535-or-patterns.md(RFC 2535,or_patterns,2018-08-29)将 or-pattern 推广为可任意嵌套,并在 "Leading |" 小节中正面讨论了前导 | 的取舍。这里给出了一个关键设计决策:

模式语法的新增应写成 pat : .. | pat "|" pat ; 还是 pat : .. | "|"? pat "|" pat ;?RFC 2535 选择了前者,即不在嵌套模式内部允许前导 |。

选择理由有四条(原文摘录):

  1. 若选择前者(不允许嵌套前导 |),未来可以再放宽为后者,但反过来不行——这是保守选择;
  2. 有 OCaml 的先例(OCaml 拒绝 Some(| 1 | 2) 这类嵌套前导竖线写法);
  3. 对宏的收益存疑——宏并不需要产出前导交替;
  4. 模式内部的前导交替被认为是糟糕的风格。

与此同时,RFC 2535 明确指出允许前导 | 有一个显著优点:让 syn 等解析库或工具稍微更容易解析 Rust 语法。

更重要的是,RFC 2535 最终在顶层模式(top_pat)中正式引入了前导 |,这正是 RFC 1925 的核心诉求。其语法定义为:

top_pat : '|'? pat<allow_top_alt> ;

并将 match 分支的产生式改写为使用 top_pat:

match_arm : maybe_outer_attrs top_pat (IF expr_nostruct)? FAT_ARROW ;

同时,let 语句、if let、while let、for 循环的语法也都改用 top_pat:

let : LET top_pat maybe_ty_ascription maybe_init_expr ';' ;
expr_if_let : IF LET top_pat '=' expr_nostruct block (ELSE block_or_if)? ;
expr_while_let : maybe_label WHILE LET top_pat '=' expr_nostruct block ;
expr_for : maybe_label FOR top_pat IN expr_nostruct block ;

也就是说:RFC 1925 提议的"match 分支开头允许可选 |"最终以 RFC 2535 的 top_pat 形式落地,并且被统一推广到所有接受顶层模式的场景。RFC 2535 的实现说明中还特别强调,如果对 c(p | q) 这样的嵌套模式做纯 DNF 展开,可能会产生模式数量上的指数级膨胀(例如 (0 | 1, 0 | 1, 0 | 1, 0 | 1) 会展开成 16 个分支),因此实际实现更倾向于一步式案例分析(one-step case analysis),而不是简单的语法糖展开——这说明前导 | 只是语法层能力,其底层匹配语义仍由 or-pattern 的统一机制承担。

教学示例:官方推荐如何向新手解释

RFC 1925 的 "How We Teach This" 章节指出,加入前导 | 的教学示例非常直接:只需指出"前导竖线是被允许的"即可。RFC 给出了一个可直接用于文档与教程的完整示例:

use Letter::*;

enum Letter {
    A,
    B,
    C,
    D,
}

fn main() {
    let a = Letter::A;
    let b = Letter::B;
    let c = Letter::C;
    let d = Letter::D;

    match a {
        A => "A",
        // 可以用 `|` 写备选分支。
        B | C | D => "B, C, or D",
    }

    match b {
        | A => "A",
        // 前导 `|` 是被允许的。
        | B
        | C
        | D => "B, C, or D",
    }
}

对比上下两个 match:第一个分支没有前导 |,第二个分支的每个备选模式前都带 |,二者语义完全相同。这个示例同时覆盖了"单一模式分支"与"多模式 or 分支"两种形态,适合作为学习资料中的对照用例。

与其他 or-patterns 语法协同使用的注意事项

前导 | 只是 or-pattern 语法的一个排版层面扩展,它不改变 | 运算符本身的优先级与结合规则。RFC 2535 明确规定:在模式中 | 的优先级最低,@(绑定模式)比 | 绑定得更紧,因此 i @ p | q 被解析为 (i @ p) | q 而不是 i @ (p | q)——这也是在顶层模式中保持向后兼容的必要条件。若你想表达 i @ (p | q),必须显式加括号。这与前导 | 的取舍是同一套语法设计中的两面:前导 | 只允许出现在顶层(top_pat),嵌套 or-pattern(Some(A | B))内部则不允许前导竖线,与 OCaml 的约束保持一致。

另外需要注意,RFC 1925 中提到前导 | "对宏有好处"(Benefits to macros),但该小节在原文中仅以 "Needs filling in"(待补充)占位,未给出具体论证,属于原提案中未展开的部分;而 RFC 2535 后来对宏场景的判断也偏保守(宏不需要产出前导交替,且 pat 宏片段说明符按 pat<no_top_alt> 匹配)。因此,宏与前导 | 的交互并未成为该语法落地的主要驱动力。

仓库中的相关文档索引

如果你想继续追踪该语法的完整演进脉络,可以在本仓库中对照阅读以下文件:

  • text/1925-optional-match-vert.md:本主题的原始提案,定义了 match_pat : '|' ? pat [ '|' pat ] * [ "if" expr ] ? 的语法改动;
  • text/2535-or-patterns.md:or-patterns 的完整设计,其中 "Leading |" 一节讨论了前导竖线的取舍,top_pat : '|'? pat<allow_top_alt> ; 是该语法正式落地的规则;
  • text/2175-if-while-or-patterns.md:将顶层 or-pattern 与可选前导 | 推广到 if let、while let、let 与 for 的 RFC;
  • lang_changes.md:Rust 语言子团队对语言变更(含语法变更)的 RFC 流程说明,前导 | 正属于此类需要 RFC 的语言层改动。

小结

RFC 1925(match_vert_prefix)用一个极小的语法改动解决了真实的排版痛点:让 or-pattern 的每个备选模式前都能对齐一个 |,从而在 Rust 中复现 F# 风格的整洁 match 代码。虽然提案本身只修改了 match_pat 一条产生式,但它的核心思想——顶层模式允许可选前导 |——最终通过 RFC 2535 的 top_pat 规则正式落地,并统一覆盖 match、let、if let、while let 与 for 等所有接受顶层模式的场景;而嵌套模式内不允许前导 | 的保守决定,则保证了与 OCaml 先例及既有模式优先级语义的一致性。理解这一演进,有助于你在实际代码中选择一致、可读的分支排版风格,并正确使用 or-patterns 语法。

登录后查看全文
rfcs