C# 9.0 的模式匹配更改
注意
本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。
功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。
可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。
我们正在考虑对 C# 9.0 的模式匹配的一小部分增强功能,这些增强功能具有自然协同作用,并且能够很好地解决许多常见的编程问题:
- https://github.com/dotnet/csharplang/issues/2925 类型模式
- https://github.com/dotnet/csharplang/issues/1350 带圆括号模式,以强制或强调新组合器的优先级
- https://github.com/dotnet/csharplang/issues/1350 联合
and
模式,要求两个模式都匹配; - https://github.com/dotnet/csharplang/issues/1350 析取
or
模式,要求两个不同模式中的任一模式来匹配; - https://github.com/dotnet/csharplang/issues/1350 否定
not
模式,要求给定模式不匹配;和 - https://github.com/dotnet/csharplang/issues/812 涉及输入值必须小于、小于或等于某个给定常数的关系模式。
带圆括号的模式
带括号模式允许程序员将括号放在任何模式周围。 这对于 C# 8.0 中的现有模式来说并不那么有用,但是新的模式组合器引入了程序员可能想要覆盖的优先级。
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
类型模式
我们允许类型作为模式:
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
这会将现有的 is-type-expression 重新设定为 is-pattern-expression,其中模式是一种类型模式,但我们不会更改编译器生成的语法树。
一个微妙的实现问题是,此语法不明确。 可以将 a.b
等字符串分析为限定名称(在类型上下文中)或虚线表达式(在表达式上下文中)。 编译器已经能够将限定名称视为与虚线表达式相同的名称,以便处理诸如 e is Color.Red
之类的内容。 编译器的语义分析将进一步扩展为能够将常量模式(例如点表达式)绑定为类型,以便将其视为绑定类型模式,以支持此构造。
在此更改之后,你将能够编写
void M(object o1, object o2)
{
var t = (o1, o2);
if (t is (int, string)) {} // test if o1 is an int and o2 is a string
switch (o1) {
case int: break; // test if o1 is an int
case System.String: break; // test if o1 is a string
}
}
关系模式
关系模式允许程序员在与常量值进行比较时表示输入值必须满足关系约束:
public static LifeStage LifeStageAtAge(int age) => age switch
{
< 0 => LifeStage.Prenatal,
< 2 => LifeStage.Infant,
< 4 => LifeStage.Toddler,
< 6 => LifeStage.EarlyChild,
< 12 => LifeStage.MiddleChild,
< 20 => LifeStage.Adolescent,
< 40 => LifeStage.EarlyAdult,
< 65 => LifeStage.MiddleAdult,
_ => LifeStage.LateAdult,
};
关系模式支持所有内置类型上的关系运算符 <
、<=
、>
和 >=
,这些内置类型支持表达式中具有两个相同类型操作数的二元关系运算符。 具体而言,我们为 sbyte
、byte
、short
、ushort
、int
、uint
、long
、ulong
、char
、float
、double
、decimal
、nint
和 nuint
支持所有这些关系模式。
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
表达式需要计算为常数值。 如果常量值 double.NaN
或 float.NaN
,则为错误。 如果表达式为 null 常量,则为错误。
当输入是定义了合适的内置二进制关系运算符的类型时,该运算符适用于将输入作为其左操作数,将给定的常数作为其右操作数,该运算符的计算被视为关系模式的含义。 否则,我们使用显式可为 null 或取消装箱转换将输入转换为表达式的类型。 如果不存在此类转换,则为编译时错误。 如果转换失败,则模式被视为不匹配。 如果转换成功,则模式匹配操作的结果是计算表达式的结果,e OP v
其中 e
是转换的输入,OP
是关系运算符,v
是常量表达式。
模式组合器
模式组合器允许使用 and
匹配两种不同的模式(可以通过重复使用 and
扩展到任意数量的模式)、使用 or
匹配两种不同模式中的任意一种(同上),或者使用 not
对模式进行否定。
组合器的常见用途将是习惯用语
if (e is not null) ...
这种模式比当前的习惯用语 e is object
更具可读性,它清楚地表示正在检查非 null 值。
and
和 or
组合器将可用于测试值范围
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
此示例说明 and
的解析优先级将比 or
更高(即 and
将结合得比 or
更紧密)。 程序员可以使用 括号模式 来明确优先级:
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
与所有模式一样,这些组合器可用于任何需要模式的上下文,包括嵌套模式、is-pattern-expression、switch-expression 以及 switch 语句中 case 标签的模式。
pattern
: disjunctive_pattern
;
disjunctive_pattern
: disjunctive_pattern 'or' conjunctive_pattern
| conjunctive_pattern
;
conjunctive_pattern
: conjunctive_pattern 'and' negated_pattern
| negated_pattern
;
negated_pattern
: 'not' negated_pattern
| primary_pattern
;
primary_pattern
: // all of the patterns forms previously defined
;
更改为 6.2.5 语法歧义
由于引入了 类型模式,因此可以在令牌 =>
之前显示泛型类型。 因此,我们将 =>
添加到 §6.2.5 语法歧义 中列出的标记集,以允许消除开始类型参数列表的 <
的歧义。 另请参阅 https://github.com/dotnet/roslyn/issues/47614。
建议的更改的待解决的问题
关系运算符的语法
and
、or
和 not
某种上下文关键字吗? 如果是这样,是否存在重大更改(例如,与它们在声明模式中用作指示符相比)。
关系运算符的语义(例如类型)
我们期望支持使用关系运算符在表达式中比较的所有基元类型。 简单情况下的含义是明确的
bool IsValidPercentage(int x) => x is >= 0 and <= 100;
但是,当输入不是这样的基元类型时,我们尝试将其转换为哪种类型?
bool IsValidPercentage(object x) => x is >= 0 and <= 100;
我们建议,当输入类型已是可比较基元时,即比较的类型。 但是,当输入不是可比基元时,我们将关系视为包括对关系右侧常量类型的隐式类型测试。 如果程序员想要支持多个输入类型,则必须显式地进行:
bool IsValidPercentage(object x) => x is
>= 0 and <= 100 or // integer tests
>= 0F and <= 100F or // float tests
>= 0D and <= 100D; // double tests
结果:关系表达式确实包含对其右侧常量类型的隐式类型测试。
类型信息从 and
的左侧流向右侧
建议在编写一个 and
组合器时,从左侧了解到的关于顶级类型的类型信息可能会流向右侧。 例如
bool isSmallByte(object o) => o is byte and < 100;
此处,第二个模式的输入类型被 and
左侧的类型收缩要求窄化。 我们将定义所有模式的类型缩小语义,如下所示。 模式 P
的窄化类型 定义如下:
- 如果
P
是一种类型模式,那么 的窄化类型 就是该类型模式的类型。 - 如果
P
是声明模式,则 窄类型 是声明模式的类型。 - 如果
P
是提供显式类型的递归模式,则 窄类型 为该类型。 - 如果
P
根据 的规则与ITuple
匹配,则收缩的类型为类型System.Runtime.CompilerServices.ITuple
。 - 如果
P
是常量模式,其中常量不是 null 常量,并且表达式没有常量表达式转换到输入类型,则收缩类型为常量的类型。 - 如果
P
是一种关系模式,其中常量表达式没有常量表达式转换到输入类型,则收缩类型是常量的类型。 - 如果
P
是or
模式,则 窄类型 是子模式 窄类型 的常见类型(如果存在此类常见类型)。 为此,通用类型算法仅考虑标识、装箱和隐式引用转换,并会考虑一系列or
模式的所有子模式(忽略带圆括号的模式)。 - 如果
P
是and
模式,则收缩类型是右侧模式的收缩类型。 此外,左侧模式的收缩类型是右侧模式的输入类型。 - 否则,
P
的 窄化类型 是P
的输入类型。
结果:已实现上述缩小语义。
变量定义和明确赋值
添加 or
和 not
模式会围绕模式变量和明确赋值产生一些有趣的新问题。 由于变量通常最多可以声明一次,因此在模式匹配时,在 or
模式的一侧声明的任何模式变量似乎都不会明确分配。 同样,当模式匹配时,在 not
模式内声明的变量也不会被明确赋值。 解决此问题的最简单方法是禁止在这些上下文中声明模式变量。 但是,这可能太严格了。 还有其他方法需要考虑。
值得考虑的一种方案是
if (e is not int i) return;
M(i); // is i definitely assigned here?
这目前不起作用,因为对于 is-pattern-expression,只有当 is-pattern-expression 为 true 时(“为 true 时绝对赋值”),模式变量才被视为明确赋值。
支持这比添加对否定条件 if
语句的支持更简单(从程序员的角度来看)。 即使我们添加了此类支持,程序员也会想知道为什么上述代码片段不起作用。 另一方面,同样的方案在 switch
中没有意义,因为在程序中没有相应的点,当 false 时明确赋值有意义。 我们是否允许在 is-pattern-expression 的情况下这样做,但不允许在允许模式的其他上下文中这样做? 这似乎不规则。
与此有关的是析取模式中明确赋值的问题。
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
我们仅在输入不为零时才期望 i
被明确指定。 但是,由于我们不知道块内的输入是否为零,所以 i
没有被明确赋值。 但是,如果我们允许 i
以不同的互斥模式声明,该怎么办?
if ((e1, e2) is (0, int i) or (int i, 0))
{
M(i);
}
在这里,变量 i
在块内被明确赋值;当找到零元素时,它从元组的另一个元素中取值。
还建议允许在事例块的每个案例中定义变量(乘数):
case (0, int x):
case (int x, 0):
Console.WriteLine(x);
为了使这一切发挥作用,我们必须仔细定义允许这种多重定义的地方,以及在什么条件下则视为这些变量已被明确赋值。
如果我们决定推迟这项工作直到以后(这是我的建议),那么我们可以在 C# 9 中这样表达。
- 在
not
或or
下,可能无法声明模式变量。
然后,我们将有时间积累一些经验,这将使我们能洞察到未来放宽限制的可能价值。
结果:模式变量不能在 not
或 or
模式下声明。
诊断、包含和详尽
这些新模式形式为可诊断程序员错误引入了许多新机会。 我们需要确定要诊断的错误类型,以及如何进行诊断。 下面是一些示例:
case >= 0 and <= 100D:
这种情况永远无法匹配(因为输入不能同时是 int
和 double
)。 当我们检测到一个永远不匹配的案例时,我们已经出现了一个错误,但它的措辞(“switch case 已经由前一个案例处理过”和“模式已经由 switch 表达式的前一个分支处理过”)在新方案中可能会产生误导。 我们可能需要修改措辞,只说模式永远不会与输入匹配。
case 1 and 2:
同样,这将是一个错误,因为值不能同时 1
和 2
。
case 1 or 2 or 3 or 1:
此情况可以匹配,但末尾的 or 1
对模式没有意义。 我建议,只要复合模式的某些连接或分离既没有定义模式变量,也没有影响匹配值的集合,我们就应该以产生错误为目标。
case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;
在这里,0 or 1 or
不会向第二种情况添加任何内容,因为这些值将由第一种情况处理。 这也应算作一个错误。
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
像这样的 switch 表达式应该被认为详尽(它处理所有可能的输入值)。
在 C# 8.0 中,只有当一个输入类型为 byte
的 switch 表达式包含一个模式匹配所有内容的最后一个arm(丢弃模式或 var 模式)时,它才被认为详尽。 在 C# 8 中,即使是对每个不同的 byte
值都有一个 arm 的 switch 表达式也不被认为详尽。 为了正确处理关系模式的详尽,我们也必须处理这种情况。 从技术上说,这是一项重大更改,但用户不太可能注意到这一点。