C# 模式匹配到 JavaScript:Lowering 完整拆解

C# 模式匹配 → JavaScript:一个 Lowering 的完整拆解

SemanticWalker.cs.Pattern.cs 把 C# 的花式匹配翻译成 JS 能懂的 typeof&&|| 和结构检查。

一、模式匹配为什么是 lowering 里最"重"的一块

Jazor 的编译器核心 SemanticWalker 是按语法特性拆文件的——循环一个文件、元组一个文件、字符串一个文件。模式匹配集中在 SemanticWalker.cs.Pattern.cs,具体代码规模随实现演进,不影响这里讨论的 lowering 原则。

为什么会这么重?

因为 C# 的模式匹配是一套高度组合化的系统。你在 C# 里能写的东西远不止 x is int n 这么简单:

var result = obj switch
{
    string { Length: > 0 } s => s,
    int and > 0 and < 100 => "small positive",
    [1, .. var rest, 3] => $"starts with 1, ends with 3, middle is {rest.Length}",
    { Name: "Alice", Age: >= 18 } => "adult Alice",
    not null => "something else",
    _ => "nothing"
};

每一个分支都是一棵独立的模式树,树里可以嵌套任意深度——属性模式里面套一个关系模式、关系模式外面再套一个二元 and 模式、列表模式里再套切片模式……Roslyn 的 IOperation 会忠实地把这种嵌套结构原样表达出来,而我要做的事情是:把它翻译成一串 JavaScript 条件表达式。

这篇文章就是把翻译过程拆开来看。

二、先看 IOperation 里模式匹配长什么样

在 Roslyn 的 IOperation 体系里,所有模式都实现了 IPatternOperation 接口。关键子类型有十几种:

IOperation 类型 C# 写法示例 含义
IConstantPatternOperation is 42 / is "hello" 常量匹配
IDeclarationPatternOperation is string s / is var x 类型声明 + 变量绑定
IDiscardPatternOperation is _ 弃元
ITypePatternOperation is string 纯类型检查
IRelationalPatternOperation is > 0 / is <= 100 关系比较
INegatedPatternOperation is not null 否定
IBinaryPatternOperation is > 0 and < 100 / is 1 or 2 二元组合 (and/or)
IRecursivePatternOperation is { Name: "Alice", Age: >= 18 } 属性模式(递归)
IListPatternOperation is [1, 2, 3] 列表模式
ISlicePatternOperation is [.., 3] / is [1, ..] 切片模式

这些模式可以出现在三个位置:

  • IIsPatternOperationx is pattern 表达式
  • IPatternCaseClauseOperationswitch 语句的 case
  • ISwitchExpressionArmOperationswitch 表达式的 arm

三、最简单的:常量模式和类型检查

先从最简单的开始——常量模式。

C# 里写 x is 42,在 IOperation 里是一个 IConstantPatternOperation,它的 Value 属性就是常量 42。翻译思路很直接:用 JS 的 === 做严格相等比较。

// C#: x is 42
// IOperation: IIsPatternOperation → IConstantPatternOperation(Value=42)
// JS:  x === 42

类型检查稍微复杂一点。C# 里 x is string 在 IOperation 里是 ITypePatternOperation,需要翻译成 JS 的 typeof 检查:

// C#: x is string
// JS:  typeof x === "string"

这里有一个映射表:string"string"int"number"bool"boolean"object"object",以此类推。对于自定义类型(比如 MyClass),需要走 instanceof 检查。

但这些是最基础的。对当前支持的成员类类型可以使用其生成的运行时 class 关系;不能把所有外部或已擦除的自定义类型都概括成 instanceof。对接口类型尤其要谨慎:如果接口在 JavaScript 侧擦除为 Object,Jazor 不会用不可靠的 instanceoftypeof 猜测。只有 Roslyn 能证明结果时,编译器才会折叠为 truefalsevalue !== null;无法证明时在使用点明确报错,并指出源静态类型和目标接口。

这条规则意味着,接口模式不是“所有场景都能生成一条 JS 类型检查”。值类型、确定实现接口的具体类型以及可证明的泛型约束可以折叠;开放或未知的对象值则保持 fail-fast。单次求值和副作用顺序仍然优先于生成代码的简洁程度。

四、声明模式:类型检查 + 变量绑定二合一

声明模式是模式匹配里最常见的形式之一:x is string s。它做了两件事:先检查 x 是不是 string,如果是,就把值绑定到新变量 s 上。

在 JS 里,你不能把"类型检查"和"变量赋值"写在一个表达式里。所以翻译方式是:先生成一个 JS 临时变量声明,然后用 typeof 检查 + 赋值 + 返回 true 的三合一 SequenceExpression(逗号表达式序列):

// C#: x is string s
// JS:  (typeof x === "string" && ((s = x), true))
//       └── 类型检查 ──┘    └── 赋值+返回true ──┘

为什么 (s = x, true) 要包在逗号表达式里?因为 JS 的赋值表达式返回的是赋值的值。如果 x 恰好是空字符串 ""(falsy),typeof x === "string" && (s = "") 整个表达式就是 false,短路会产生错误的匹配结果。用逗号表达式 (s = x, true) 保证这个子表达式始终返回 true——类型已经检查过了,赋值只是副作用。

五、属性模式:递归的入口

属性模式(RecursivePattern)是真正让模式匹配"重"起来的元凶。C# 里写:

x is { Name: "Alice", Age: >= 18 }

这棵 IOperation 树是:

IRecursivePatternOperation
├── MatchedType: Person
├── PropertySubpatterns:
│   ├── IPropertySubpatternOperation
│   │   ├── Member: Name (属性符号)
│   │   └── Pattern: IConstantPatternOperation ("Alice")
│   └── IPropertySubpatternOperation
│       ├── Member: Age (属性符号)
│       └── Pattern: IRelationalPatternOperation (>= 18)

翻译成 JS 时,需要"先缓存对象以避免重复求值,再逐个属性比较":

// C#: x is { Name: "Alice", Age: >= 18 }
// JS:
//   (__temp = x,
//    __temp !== null &&
//    __temp.Name === "Alice" &&
//    __temp.Age >= 18)

注意两点:

  1. 对象只求值一次。在 JS 里如果你写成 x.Name === "Alice" && x.Age >= 18x 被求值了两次。如果 x 是一个有副作用的属性访问(比如 GetPerson()),求值两次是语义错误。所以 Jazor 的策略是先生成一个临时变量 __temp,把 x 缓存进去。

  2. 空值保护{ Name: "Alice" } 模式要求被匹配的对象不能是 null——否则访问 .Name 会抛异常。所以在属性检查之前插入 __temp !== null

如果属性模式嵌套得更深,比如 x is { Address: { City: "Beijing" } },翻译逻辑会递归展开:

(__temp = x,
 __temp !== null &&
 (__temp2 = __temp.Address,
  __temp2 !== null &&
  __temp2.City === "Beijing"))

六、二元模式:and / or 的短路处理

andor 模式很直观,但在 lowering 时要特别注意短路求值。

is > 0 and < 100&& 连接:

x > 0 && x < 100

is 1 or 2 or 3|| 连接:

x === 1 || x === 2 || x === 3

但这里有一个细节:如果 or 的某个分支包含声明模式(比如 is string s or int i),C# 只允许使用其中一个声明的变量(definitely assigned 的那个)。在 JS 翻译里,我们不需要模拟这个"确实赋值"的流程分析——因为 JS 没有这个语义——但我们必须在 lowering 时确保每个分支的变量声明被正确放置在各自的短路分支内。

七、列表模式:最复杂的翻译

列表模式是做 lowering 最耗时的部分。C# 里写:

x is [1, 2, 3]          // 固定长度
x is [1, .., 3]         // 切片
x is [> 0, .. var rest] // 元素模式 + 切片绑定

翻译成 JS 需要好几步:

第一步:长度检查。 对于 [1, 2, 3],先检查 x.length === 3。对于 [1, .., 3],先检查 x.length >= 2(前后各一个,中间任意多)。

第二步:元素匹配。 按索引逐一检查元素。x[0] === 1 && x[2] === 3

第三步:切片绑定。 .. var rest 变成 rest = x.slice(1, -1)

完整翻译:

// C#: x is [> 0, .. var rest, 3]
// JS:
//   (Array.isArray(x) &&
//    x.length >= 2 &&
//    x[0] > 0 &&
//    x[x.length - 1] === 3 &&
//    ((rest = x.slice(1, -1)), true))

注意 Array.isArray(x) 的前置检查——因为访问 .length 和索引访问假定被匹配对象是数组。这个检查是列表模式独有的。

八、switch 表达式:模式匹配的综合体

switch 表达式是把上面的所有模式类型组合起来的地方:

var result = obj switch
{
    int n when n > 0 => "positive int",
    string s => $"string: {s}",
    null => "nothing",
    _ => "unknown"
};

翻译成 JS 时,Jazor 的策略是用一个 IIFE(立即执行函数表达式)

var result = (() => {
    var __input = obj;
    if (typeof __input === "number" && __input > 0) {
        var n = __input;
        return "positive int";
    }
    if (typeof __input === "string") {
        var s = __input;
        return "string: " + s;
    }
    if (__input === null) return "nothing";
    return "unknown";
})();

用 IIFE 的好处是:

  • 变量声明(ns)的作用域只在各自的分支内,不会泄漏到外面
  • when 子句天然变成 if 里的附加条件(n > 0
  • 语义和 C# 的 switch 表达式完全一致——按顺序匹配,命中即返回

如果 switch 分支里包含了 await(比如 async var x => await Something(x)),IIFE 会变成 async 箭头函数。这是通过 ContainsAwaitOperation(operation) 检测的。

九、求值顺序:Lowering 的第一优先级

在整个模式匹配的 lowering 过程中,有一条红线贯穿始终:求值顺序不能变。

C# 里 x is { A: > 0 } and { B: < 10 },求值顺序是先检查 A 再检查 B,短路意味着如果 A 不满足,B 根本不会被求值。翻译到 JS 时,不能为了"好看"把条件重新排序——你必须保持完全相同的 && 链。

Jazor 对这条红线的实现方式是在所有需要缓存的地方插入 SequenceExpression(逗号表达式)来保证"先算这个、再算那个"的顺序。它看起来会多生成一些临时变量,但语义是正确的。

这和 Jazor 编译器的总原则一致:行为保真优先于生成代码的美观。

十、小结

C# 的模式匹配是一套表达能力极强的特性。把它翻译成 JavaScript,本质上是在做一层语义展开——把编译器替你做的类型推理、属性遍历、长度判断、短路计算,一个不漏地重新表达为 JavaScript 的基础操作。

单看 SemanticWalker.cs.Pattern.cs 的实现,你能看到模式匹配 lowering 的全貌:

  • 十几种 IPatternOperation 各自对应不同的 JS 翻译策略
  • 嵌套模式通过递归 Visitor 自然展开
  • 求值顺序通过 SequenceExpression 和临时变量缓存来保证
  • switch 表达式用 IIFE 隔离作用域
  • and/or 忠实映射为 &&/||
  • 接口擦除模式只在 Roslyn 可证明时做 compile-time fold,否则显式失败
  • 列表模式自己插入 Array.isArray 前置检查

如果你也在做类似的编译器工作,模式匹配应当是你在设计 lowering 策略时最早考虑清楚的部分——因为它在 IOperation 树里的组合复杂度,会在后续每一个新增的模式类型上成倍放大。

项目地址:https://github.com/devhxj/Jazor

posted @ 2026-05-11 13:36  形而下晓寒  阅读(9)  评论(0)    收藏  举报