11 模式和模式匹配

11.1 常规

模式是一种语法形式,可用于is运算符(§12.14.12)、switch_statement§13.8.3),在switch_expression§12.11)中表达要对其比较传入数据的数据的形状。 模式可能是递归的,以便数据的各个部分可以与 子模式匹配。

针对多个上下文中的值测试模式:

  • 在 switch 语句中,事例标签的 模式 根据 switch 语句的 表达式 进行测试。
  • is-pattern 运算符中,右侧的 模式 针对左侧的表达式进行测试。
  • 在 switch 表达式中,switch_expression_arm模式针对 switch-expression 左侧的表达式进行测试。
  • 在嵌套上下文中,根据模式窗体,针对从属性、字段或从其他输入值编制索引的值测试 子模式

对其测试模式的值称为 模式输入值

11.2 模式窗体

11.2.1 常规

模式可能具有以下形式之一:

pattern
    : declaration_pattern
    | constant_pattern
    | var_pattern
    | positional_pattern
    | property_pattern
    | discard_pattern
    ;

某些 模式可能会导致局部变量的声明。

每个模式窗体定义可应用于该模式的输入值的类型集。 如果P模式是模式可能匹配的类型之一,则模式TT类型。 如果某个模式出现在程序中以匹配类型的P模式输入值(§11.1T如果P不适用T)。

示例:以下示例生成编译时错误,因为编译时类型 vTextReader. 类型的 TextReader 变量永远不能具有与以下项兼容的 string值:

TextReader v = Console.In; // compile-time type of 'v' is 'TextReader'
if (v is string) // compile-time error
{
    // code assuming v is a string
}

但是,由于编译时类型 vobject,因此以下代码不会生成编译时错误。 类型的 object 变量可以具有与引用兼容的 string值:

object v = Console.In;
if (v is string s)
{
    // code assuming v is a string
}

end 示例

每个模式窗体定义模式 在运行时匹配 值的一组值。

未指定模式匹配期间对作和副作用的计算顺序(对 Deconstruct方法的 System.ITuple调用、属性访问和调用)。

11.2.2 声明模式

declaration_pattern用于测试某个值是否具有给定类型,如果测试成功,可以选择在该类型的变量中提供该值。

declaration_pattern
    : type simple_designation
    ;
simple_designation
    : discard_designation
    | single_variable_designation
    ;
discard_designation
    : '_'
    ;
single_variable_designation
    : identifier
    ;

使用令牌_的simple_designation应被视为discard_designation而不是single_variable_designation

值的运行时类型使用 is-type 运算符(§12.14.12.1)中指定的相同规则针对模式中的类型进行测试。 如果测试成功,则模式 与该值匹配 。 如果 类型 为可以为 null 的值类型(§8.3.12)或可为空引用类型(§8.9.3),则为编译时错误。 此模式形式永远不会与值 null 匹配。

注意:is-type 表达式 e is T 和声明模式 e is T _ 在不是可以为 null 的类型时 T 是等效的。 尾注

给定模式输入值 (§11.1e,如果simple_designation discard_designation,则表示放弃(§9.2.9.2),e 的值不绑定到任何内容。 (虽然具有该名称_的声明变量可能位于该点的作用域内,但在此上下文中看不到该命名变量。否则,如果single_variable_designation simple_designation,则会引入由给定标识符命名的给定类型的局部变量(§9.2.9)。 当模式与值匹配时,向该局部变量分配模式输入值的值。

模式输入值和给定类型的静态类型的某些组合被视为不兼容,并导致编译时错误。 如果存在标识转换、隐式或显式引用转换、装箱转换或从中取消装箱转换或E打开类型(§8.4.3),则表示TET 声明模式命名类型T适用于与该类型兼容的每种类型EET

注意:在检查可能为结构或类类型的类型时,对开放类型的支持最有用,并且要避免装箱。 尾注

示例:声明模式可用于执行引用类型的运行时类型测试,并替换成语

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

稍微简洁一点

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

end 示例

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

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

语句的条件iftrue运行时,变量v保存块内类型的3int。 块之后,变量 v 位于范围内,但未明确分配。 end 示例

11.2.3 常量模式

constant_pattern用于针对给定常量值测试模式输入值(§11.1)的值。

constant_pattern
    : constant_expression
    ;

如果存在从常量表达式P到该类型的隐式转换,则常量模式T类型P

对于常量模式 P,其 转换的值

  • 如果模式输入值的类型是整型类型或枚举类型,则模式的常量值转换为该类型;否则
  • 如果模式输入值的类型是整型类型或枚举类型的可为 null 版本,则模式的常量值转换为其基础类型;否则
  • 模式常量值的值。

给定模式输入值 e 和具有转换值 v 的常量模式P

  • 如果 e 具有整型类型或枚举类型,或其中一种可为 null 的形式,并且 v 具有整型类型,则模式P;否则
  • 如果返回,则模式P

示例switch 以下方法中的语句在其事例标签中使用五个常量模式。

static decimal GetGroupTicketPrice(int visitorCount)
{
    switch (visitorCount) 
    {
        case 1: return 12.0m;
        case 2: return 20.0m;
        case 3: return 27.0m;
        case 4: return 32.0m;
        case 0: return 0.0m;
        default: throw new ArgumentException(...);
    }
}

end 示例

11.2.4 Var 模式

var_pattern 匹配每个值。 也就是说,具有 var_pattern 的模式匹配操作始终成功。

var_pattern适用于每种类型。

var_pattern
    : 'var' designation
    ;
designation
    : simple_designation
    | tuple_designation
    ;
tuple_designation
    : '(' designations? ')'
    ;
designations
    : designation (',' designation)*
    ;

给定模式输入值(§11.1e,如果指定为discard_designation,则表示放弃(§9.2.9.2),e 的值不绑定到任何内容。 (尽管具有该名称的声明变量可能位于该点的范围中,但在此上下文中看不到该命名变量。否则,如果指定为single_variable_designation,在运行时,e 的值将绑定到该名称的新引入局部变量(§9.2.9),其类型为 e 的静态类型,并将模式输入值分配给该局部变量。

如果名称var绑定到使用var_pattern的类型,则为错误。

如果指定tuple_designation,则模式等效于表单(var指定positional_pattern§11.2.5),... ) 其中 ,指定项是在 tuple_designation中找到的。 例如,模式 var (x, (y, z)) 等效于 (var x, (var y, var z))

11.2.5 位置模式

positional_pattern检查输入值是否不是null,调用适当的Deconstruct方法(§12.7),并针对生成的值执行进一步的模式匹配。 如果输入值的类型与包含 Deconstruct的类型相同,或者输入值的类型是元组类型,或者输入值的类型是元组类型,或者输入值的类型是 objectSystem.ITuple 表达式 System.ITuple的运行时类型,则它还支持类似于元组的模式语法(没有提供类型)。

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

给定输入值与模式 类型(子模式)的匹配项后, 可以通过搜索类型 中搜索可访问声明 Deconstruct 并选择其中一个方法,并使用与解构声明相同的规则来选择一个方法。 如果positional_pattern省略类型、没有标识符的单个子模式、没有property_subpattern且没有simple_designation,则错误。 这在括号和positional_patternconstant_pattern之间消除歧义。 为了提取要与列表中的模式匹配的值,

  • 如果省略 类型 并且输入表达式的类型为元组类型,则子模式的数目应与元组的基数相同。 每个元组元素都与相应的 子模式匹配,如果所有这些元素都成功,匹配将成功。 如果任何 子模式 都有 标识符,则应将元组元素命名为元组类型中相应位置的元组元素。
  • 否则,如果适合 Deconstruct 作为 类型的成员存在,则如果输入值的类型与 类型不兼容,则为编译时错误。 在运行时,将针对 类型测试输入值。 如果此作失败,则位置模式匹配失败。 如果成功,则输入值将转换为此类型,并使用 Deconstruct 新的编译器生成的变量调用,以接收输出参数。 收到的每个值都与相应的 子模式匹配,如果所有这些值都成功,则匹配成功。 如果任一 子模式 具有 标识符,则应将参数命名为相应的位置 Deconstruct
  • 否则,如果省略 类型 ,并且输入值为类型 object 或某种类型,可通过隐式引用转换转换为 System.ITuple 某种类型,并且子模式之间不会显示 标识符 ,则匹配将使用 System.ITuple
  • 否则,模式是编译时错误。

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

示例:此处,我们将解构表达式结果,并将结果值与相应的嵌套模式匹配:

static string Classify(Point point) => point switch
{
    (0, 0) => "Origin",
    (1, 0) => "positive X basis end",
    (0, 1) => "positive Y basis end",
    _ => "Just a point",
};

public readonly struct Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) => (X, Y) = (x, y);
    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

end 示例

示例:元组元素和析构参数的名称可用于位置模式,如下所示:

var numbers = new List<int> { 10, 20, 30 };
if (SumAndCount(numbers) is (Sum: var sum, Count: var count))
{
    Console.WriteLine($"Sum of [{string.Join(" ", numbers)}] is {sum}");
}

static (double Sum, int Count) SumAndCount(IEnumerable<int> numbers)
{
    int sum = 0;
    int count = 0;
    foreach (int number in numbers)
    {
        sum += number;
        count++;
    }
    return (sum, count);
}

生成的输出为

Sum of [10 20 30] is 60

end 示例

11.2.6 属性模式

property_pattern检查输入值是否不null,并且以递归方式匹配通过使用可访问属性或字段提取的值。

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

如果property_pattern的任何子模式不包含标识符,则为错误。

如果 类型 为可以为 null 的值类型(§8.3.12)或可为空引用类型(§8.9.3),则为编译时错误。

注意:null 检查模式超出了一个普通的属性模式。 若要检查字符串 s 是否为非 null,可以编写以下任一形式:

#nullable enable
string s = "abc";
if (s is object o) ...  // o is of type object
if (s is string x1) ... // x1 is of type string
if (s is {} x2) ...     // x2 is of type string
if (s is {}) ...

end note给定表达式 e 与模式类型{property_pattern_list匹配}时,如果表达式 e与类型指定的 T 类型不兼容,则为编译时错误。 如果该类型不存在,则假定该类型为 e 的静态类型。 每个出现在其property_pattern_list左侧的标识符应指定一个可访问的可读属性或 T 字段。如果存在property_patternsimple_designation,则声明 T 类型的模式变量。

在运行时,表达式针对 T 进行测试。如果此作失败,则属性模式匹配失败,结果为 false。 如果成功,则读取每个 property_subpattern 字段或属性,并且其值与相应的模式匹配。 整个匹配 false 的结果仅当其中任何一项的结果为 false时。 未指定子模式的匹配顺序,失败的匹配可能无法在运行时测试所有子模式。 如果匹配成功,并且property_pattern的simple_designationsingle_variable_designation,则会为声明的变量分配匹配的值。

property_pattern可用于与匿名类型进行模式匹配。

示例:

var o = ...;
if (o is string { Length: 5 } s) ...

end 示例

示例:运行时类型检查和变量声明可以添加到属性模式,如下所示:

Console.WriteLine(TakeFive("Hello, world!"));  // output: Hello
Console.WriteLine(TakeFive("Hi!"));            // output: Hi!
Console.WriteLine(TakeFive(new[] { '1', '2', '3', '4', '5', '6', '7' }));  // output: 12345
Console.WriteLine(TakeFive(new[] { 'a', 'b', 'c' }));  // output: abc

static string TakeFive(object input) => input switch
{
    string { Length: >= 5 } s => s.Substring(0, 5),
    string s => s,
    ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
    ICollection<char> symbols => new string(symbols.ToArray()),
    null => throw new ArgumentNullException(nameof(input)),
    _ => throw new ArgumentException("Not supported input type."),
};

生成的输出为

Hello
Hi!
12345
abc

end 示例

11.2.7 放弃模式

每个表达式都与放弃模式匹配,这会导致放弃表达式的值。

discard_pattern
    : '_'
    ;

表单isrelational_expression模式的relational_expression中或作为switch_label模式的模式使用放弃模式是编译时错误。

注意:在这些情况下,若要匹配任何表达式,请使用带放弃var _的var_pattern尾注

示例:

Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday));
Console.WriteLine(GetDiscountInPercent(null));
Console.WriteLine(GetDiscountInPercent((DayOfWeek)10));

static decimal GetDiscountInPercent(DayOfWeek? dayOfWeek) => dayOfWeek switch
{
    DayOfWeek.Monday => 0.5m,
    DayOfWeek.Tuesday => 12.5m,
    DayOfWeek.Wednesday => 7.5m,
    DayOfWeek.Thursday => 12.5m,
    DayOfWeek.Friday => 5.0m,
    DayOfWeek.Saturday => 2.5m,
    DayOfWeek.Sunday => 2.0m,
    _ => 0.0m,
};

生成的输出为

5.0
0.0
0.0

此处,使用放弃模式来处理 null 任何没有相应枚举成员的 DayOfWeek 整数值。 这可以保证 switch 表达式处理所有可能的输入值。 end 示例

11.3 模式子建议

在 switch 语句中,如果事例的模式被前面的未保护事例集(§13.8.3)子化,则为错误。 非正式地说,这意味着任何输入值都将与前面的案例之一匹配。 以下规则定义一组模式何时将给定模式子化:

P 常量K

如果满足以下任一条件,则一组模式Q会子化模式P

  • P是一个常量模式,集中Q的任何模式都与转换后的值匹配P
  • P是 var 模式,模式集Q对于模式输入值的类型(§11.4
  • P是具有类型的T声明模式,并且该类型Q§11.4)的模式T集是详尽的。

11.4 模式详尽

非正式情况下,如果对于该类型(非 null)的每个可能值,则一组模式对于类型而言都是详尽的,则集中的某些模式适用。 以下规则定义类型一组模式 何时详尽

如果满足以下任一条件,则一组模式Q的:

  1. T 是整数或枚举类型,或其中一个的可为 null 版本,对于'不可为 null 的基础类型的每个可能值 T,某些 Q 模式将匹配该值;或
  2. 某些模式 Qvar 模式;或
  3. 某些模式Q,并且存在标识转换、隐式引用转换或从D中转换到T的装箱转换。

示例:

static void M(byte b)
{
    switch (b) {
        case 0: case 1: case 2: ... // handle every specific value of byte
            break;
        // error: the pattern 'byte other' is subsumed by the (exhaustive)
        // previous cases
        case byte other: 
            break;
    }
}

end 示例