递归模式匹配

注意

本文是特性规范。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。

功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。

可以在有关规范的文章中了解更多有关将功能规范子块纳入 C# 语言标准的过程。

支持者问题:https://github.com/dotnet/csharplang/issues/45

总结

C# 的模式匹配扩展使用户可以享受代数数据类型和功能语言中的许多模式匹配优势,同时这些扩展能够自然融入该语言的整体风格。 此方法的元素灵感来自编程语言中的相关功能:F#Scala

详细设计

Is 表达式

为了测试表达式是否符合 is,运算符 已经被扩展。

relational_expression
    : is_pattern_expression
    ;
is_pattern_expression
    : relational_expression 'is' pattern
    ;

在 C# 规范中,此形式的 relational_expression 是对现有形式的补充。 如果 relational_expression 位于 is 令牌的左侧但未指定一个值或没有类型,则这是一个编译时错误。

模式的每个标识符都会引入一个新的局部变量,该局部变量在 is 运算符为 true明确赋值(即在 true 时明确赋值)。

注意:从技术上讲,is-expression 中的类型之间存在歧义,二者任一都可能是限定标识符的有效解析。 我们试图将其绑定为类型,以与该语言的早期版本兼容;只有当失败时,我们才会像在其他上下文中处理表达式一样,将其解析为找到的首个内容(必须是常量或类型)。 这种歧义只出现在 is 表达式的右侧。

模式

模式在 is_pattern 运算符、switch_statementswitch_expression 中用于描述传入数据(即我们所谓的“输入值”)进行比较的数据的形状。 模式可以是递归的,因此部分数据可以与子模式匹配。

pattern
    : declaration_pattern
    | constant_pattern
    | var_pattern
    | positional_pattern
    | property_pattern
    | discard_pattern
    ;
declaration_pattern
    : type simple_designation
    ;
constant_pattern
    : constant_expression
    ;
var_pattern
    : 'var' designation
    ;
positional_pattern
    : type? '(' subpatterns? ')' property_subpattern? simple_designation?
    ;
subpatterns
    : subpattern
    | subpattern ',' subpatterns
    ;
subpattern
    : pattern
    | identifier ':' pattern
    ;
property_subpattern
    : '{' '}'
    | '{' subpatterns ','? '}'
    ;
property_pattern
    : type? property_subpattern simple_designation?
    ;
simple_designation
    : single_variable_designation
    | discard_designation
    ;
discard_pattern
    : '_'
    ;

声明模式

declaration_pattern
    : type simple_designation
    ;

declaration_pattern 都测试表达式是否为给定类型;如果测试成功,则将其转换为该类型。 如果指定为 single_variable_designation,则这可能会引入由给定标识符命名的给定类型的局部变量。 当模式匹配操作的结果为 时,该局部变量true

此表达式的运行时语义是,它根据模式中的类型测试左 relational_expression 操作数的运行时类型。 如果它是该运行时类型(或某些子类型),而不是 null,则 is operator 的结果是 true

某些左侧的静态类型和指定类型的组合被视为不兼容,这会导致编译时错误。 如果存在从 E 的标识转换、隐式引用转换、装箱转换、显式引用转换或取消装箱转换,或者其中一种类型是开放类型,那么静态类型 T 的值被称为与类型 ET。 如果类型 E 的输入与类型模式兼容,将会是编译时错误。则这是一个编译时错误。

该类型模式可用于对引用类型进行运行时类型测试,并取代习惯用语

var v = expr as Type;
if (v != null) { // code using v

稍微简洁一点

if (expr is Type v) { // code using v

如果类型为可以为 null 的值类型,则会出错。

类型模式可用于测试可为 null 类型的值:如果值为非 null,且 Nullable<T> 的类型为 T 或者为 T2 id 的某些基类型或接口,那么类型 T2(或装箱后的 T)的值与类型模式 T 匹配。 例如,在代码片段中

int? x = 3;
if (x is int v) { // code using v

在运行时,if 语句的条件是 true,变量 v 在块内保存类型 3 的值 int。 在块之后,变量 v 仍在作用域内,但未被明确赋值。

常量模式

constant_pattern
    : constant_expression
    ;

常量模式根据常量值测试表达式的值。 常量可以是任何常量表达式,例如字面量、已声明 const 变量的名称或枚举常量。 当输入值不是开放类型时,常量表达式会被隐式转换为与之匹配的表达式类型;如果输入值的类型与常量表达式的类型在模式下不兼容,则模式匹配操作会出错。

如果 object.Equals(c, e) 返回 true,则 c 模式将被视为与转换后的输入值 e 匹配。

在新编写的代码中,我们预计 e is null 是测试 null 的最常见方式,因为它无法调用用户定义的 operator==

Var 模式

var_pattern
    : 'var' designation
    ;
designation
    : simple_designation
    | tuple_designation
    ;
simple_designation
    : single_variable_designation
    | discard_designation
    ;
single_variable_designation
    : identifier
    ;
discard_designation
    : _
    ;
tuple_designation
    : '(' designations? ')'
    ;
designations
    : designation
    | designations ',' designation
    ;

如果指定simple_designation,则表达式 e 与模式匹配。 换句话说,与 var 模式的匹配始终在 simple_designation 时成功。 如果 simple_designationsingle_variable_designation,则 e 的值将会被绑定到一个新引入的局部变量。 本地变量的类型是 e 的静态类型。

如果 designationtuple_designation,则模式相当于 (vardesignation, ... ) 形式的 positional_pattern,其中 designation 是在 tuple_designation 中找到的。 例如,模式 var (x, (y, z)) 等效于 (var x, (var y, var z))

如果名称 var 与类型绑定,则会出错。

弃元模式

discard_pattern
    : '_'
    ;

表达式 e 始终与模式 _ 匹配。 换句话说,每个表达式都与“弃元模式”匹配。

弃元模式不能作为 is_pattern_expression 的模式使用。

位置模式

位置模式会检查输入值不是 null,调用适当的 Deconstruct 方法,并对结果值执行进一步的模式匹配。 如果输入值的类型与包含 Deconstruct 的类型相同,或者输入值的类型是元组类型,或者输入值的类型是 objectITuple,并且表达式的运行时类型实现了 ITuple,那么它还支持类似元组的模式语法(不提供类型)。

positional_pattern
    : type? '(' subpatterns? ')' property_subpattern? simple_designation?
    ;
subpatterns
    : subpattern
    | subpattern ',' subpatterns
    ;
subpattern
    : pattern
    | identifier ':' pattern
    ;

如果省略 类型,我们将其视为输入值的静态类型。

鉴于输入值与模式类型(subpattern_list) 匹配,通过在类型中搜索 Deconstruct 的可访问声明,并使用与析构声明相同的规则从中选择一个来选择方法。

如果 positional_pattern 省略了类型,有一个 subpattern 没有标识符,没有 property_subpattern,也没有 simple_designation,则这是一个错误。 这消除了括号括起来的 constant_patternpositional_pattern 之间的歧义。

为了提取要与列表中的模式进行匹配的值,

  • 如果省略了 类型 且输入值的类型为元组类型,那么子模式的数量需要与元组的元素个数相同。 每个元组元素都与相应的 子模式匹配,如果所有这些匹配都成功,整个匹配过程即告成功。 如果任何 subpattern 具有标识符,则必须在元组类型中的相应位置命名一个元组元素。
  • 否则,如果存在一个适当的 Deconstruct 作为类型的成员,并且如果输入值的类型与类型模式不兼容,则这是一个编译时错误。 在运行时,针对类型测试了输入值。 如果失败,则位置模式匹配失败。 如果成功,输入值将被转换为这种类型,Deconstruct 将被调用,并使用编译器生成的新变量接收 out 参数。 接收到的每个值都会与相应的子模式进行匹配,如果全部匹配成功,则匹配成功。 如果任何子模式有一个标识符,则必须在 Deconstruct 的相应位置命名一个参数。
  • 否则,如果省略了 类型,且输入值的类型为 objectITuple 或可通过隐式引用转换为 ITuple 的某种类型,且子模式中未出现 标识符,则采用 ITuple进行匹配。
  • 否则,该模式将在编译时出错。

运行时匹配子模式的顺序未作规定,匹配失败时可能不会尝试匹配所有子模式。

示例

此示例使用了本规范中描述的许多功能

    var newState = (GetState(), action, hasKey) switch {
        (DoorState.Closed, Action.Open, _) => DoorState.Opened,
        (DoorState.Opened, Action.Close, _) => DoorState.Closed,
        (DoorState.Closed, Action.Lock, true) => DoorState.Locked,
        (DoorState.Locked, Action.Unlock, true) => DoorState.Closed,
        (var state, _, _) => state };

属性模式

属性模式确保输入值不为 null,并递归匹配通过可访问属性或字段提取的值。

property_pattern
    : type? property_subpattern simple_designation?
    ;
property_subpattern
    : '{' '}'
    | '{' subpatterns ','? '}'
    ;

如果 property_pattern 的任何 子模式 不包含一个 标识符(它必须是具有 标识符的第二种形式),则这是一个错误。 最后一个子模式后的逗号为可选。

请注意,null 检查模式不属于普通属性模式。 要检查字符串 s 是否为非空,可以写成以下任何一种形式

if (s is object o) ... // o is of type object
if (s is string x) ... // x is of type string
if (s is {} x) ... // x is of type string
if (s is {}) ...

鉴于表达式 e 与模式类型{property_pattern_list} 匹配,如果表达式 e类型指定的类型 T模式不兼容,则这是一个编译时错误。 如果没有类型,我们就认为它是 e 的静态类型。 如果存在 标识符,则声明一个类型为 的模式变量。 每个出现在其 property_pattern_list 左侧的标识符都必须指定一个可访问且可读的属性或 T字段。如果简单指示符 simple_designation 存在于 property_pattern 中,则它定义了一个 T类型的模式变量。

在运行时,表达式将根据 T 进行测试。如果测试失败,则属性模式匹配失败,结果为 false。 如果成功,则读取每个 property_subpattern 字段或属性,并将其值与相应的模式进行匹配。 只有当其中任何一项的结果是 false 时,整个匹配的结果才会是 false。 没有指定匹配子模式的顺序,运行时匹配失败可能无法匹配所有子模式。 如果匹配成功,并且 property_patternsimple_designationsingle_variable_designation,则它定义一个类型 T 的变量,该变量被赋予了匹配的值。

注意:属性模式可用于匿名类型的模式匹配。

示例
if (o is string { Length: 5 } s)

Switch 表达式

添加了 switch_expression,以支持表达式上下文的 switch 类似语义。

C# 语言语法通过以下语法产生式进行扩充:

multiplicative_expression
    : switch_expression
    | multiplicative_expression '*' switch_expression
    | multiplicative_expression '/' switch_expression
    | multiplicative_expression '%' switch_expression
    ;
switch_expression
    : range_expression 'switch' '{' '}'
    | range_expression 'switch' '{' switch_expression_arms ','? '}'
    ;
switch_expression_arms
    : switch_expression_arm
    | switch_expression_arms ',' switch_expression_arm
    ;
switch_expression_arm
    : pattern case_guard? '=>' expression
    ;
case_guard
    : 'when' null_coalescing_expression
    ;

禁止将 switch_expression 用作 expression_statement

我们正在考虑在今后的修订中放宽这一规定。

switch_expression 的类型是出现在 switch_expression_arm=> 标记右侧的表达式的最佳常见类型 (§12.6.3.15),如果存在这种类型,则 switch 表达式的每个部分中的表达式都可以隐式转换为该类型。 此外,我们添加了一个新的 switch 表达式转换,这是从 switch 表达式到每个类型 T 的预定义隐式转换,对于每种类型,都存在从每个部分的表达式到 T 的隐式转换。

如果某些 switch_expression_arm 的模式不能影响结果,因为一些以前的模式和防护将始终匹配,那么这就是一个错误。

如果 switch 表达式的某个部分处理其输入的每个值,则 switch 表达式被称为详尽。 如果 switch 表达式未详尽,编译器会生成警告。

在运行时,switch_expression 的结果是第一个 switch_expression_arm表达式的值,其中 switch_expression 左侧的表达式与 switch_expression_arm 的模式相匹配,对于该表达式,switch_expression_armcase_guard(如果存在)的计算结果为 true。 如果没有此类 switch_expression_arm,则 switch_expression 将抛出一个异常实例 System.Runtime.CompilerServices.SwitchExpressionException

打开元组字面量时的可选括号

若要使用 switch_statement 打开元组字面量,必须编写看似多余的括号。

switch ((a, b))
{

允许

switch (a, b)
{

当打开的表达式是元组文本时,switch 语句的括号是可选的。

模式匹配中的评估顺序

赋予编译器在模式匹配过程中重新排序操作的灵活性,可以用于提高模式匹配的效率。 (不会强制执行)要求是,在模式中访问的属性和析构方法必须是“纯”(无副作用、幂等等)。 这并不意味着我们要把纯粹性作为一个语言概念添加进去,只是说我们要允许编译器灵活地对操作进行重新排序。

Resolution 2018-04-04 LDM:已确认:允许编译器对 Deconstruct中的调用、属性访问以及 ITuple中的方法调用进行重新排序,并可以假设多个调用返回的值是相同的。 编译器不应调用不能影响结果的函数,我们今后在对编译器生成的求值顺序进行任何修改之前都会非常谨慎。

一些可能的优化措施

模式匹配编译时可以利用模式中的常见部分。 例如,如果 switch_statement 中两个连续模式的顶层类型测试为同一类型,则生成的代码可以跳过第二个模式的类型测试。

当某些模式是整数或字符串时,编译器可以生成与早期版本语言中 switch 语句相同类型的代码。

关于这类优化的详细信息,请参阅 [Scott and Ramsey (2000)]