模式匹配Pattern Matching

模式可测试值是否具有特定形状,并且可以在值具有匹配形状时从值提取信息。Patterns test that a value has a certain shape, and can extract information from the value when it has the matching shape. 模式匹配为当前已使用的算法提供了更简洁的语法。Pattern matching provides more concise syntax for algorithms you already use today. 你已使用现有语法创建了模式匹配算法。You already create pattern matching algorithms using existing syntax. 编写了测试值的 ifswitch 语句。You write if or switch statements that test values. 随后,在这些语句匹配时,可从该值提取并使用信息。Then, when those statements match, you extract and use information from that value. 新的语法元素是你已熟悉的语句的扩展:isswitchThe new syntax elements are extensions to statements you're already familiar with: is and switch. 这些新扩展将测试值与提取该信息合并在一起。These new extensions combine testing a value and extracting that information.

本文将介绍新语法,以展示它是如何实现可读的简洁代码的。In this article, we'll look at the new syntax to show you how it enables readable, concise code. 模式匹配可实现数据与代码分开的惯例,这与面向对象的设计不同,后者紧密耦合数据和控制它们的方法。Pattern matching enables idioms where data and the code are separated, unlike object-oriented designs where data and the methods that manipulate them are tightly coupled.

为了说明这些新的惯例,我们使用通过模式匹配语句表示几何形状的结构。To illustrate these new idioms, let's work with structures that represent geometric shapes using pattern matching statements. 你可能熟悉如何通过生成类层次结构和创建虚方法和重写方法,根据对象的运行时类型来自定义对象行为。You're probably familiar with building class hierarchies and creating virtual methods and overridden methods to customize object behavior based on the runtime type of the object.

对于未在类层次结构中进行结构化的数据,无法使用这些技术。Those techniques aren't possible for data that isn't structured in a class hierarchy. 当数据和方法分开时,需要其他工具。When data and methods are separate, you need other tools. 新的模式匹配构造可实现更简洁的语法,以基于这些数据的任何条件来检查数据和操作控制流。The new pattern matching constructs enable cleaner syntax to examine data and manipulate control flow based on any condition of that data. 你已编写了测试变量值的 if 语句和 switchYou already write if statements and switch that test a variable's value. 你编写了测试变量类型的 is 语句。You write is statements that test a variable's type. 模式匹配为这些语句添加了新功能。Pattern matching adds new capabilities to those statements.

在本文中,你将生成计算不同几何形状的面积的方法。In this article, you'll build a method that computes the area of different geometric shapes. 但无需求助于面向对象的技术,也无需为不同的形状生成类层次结构。But, you'll do it without resorting to object-oriented techniques and building a class hierarchy for the different shapes. 你会改用模式匹配。You'll use pattern matching instead. 执行此示例时,将此代码与它作为对象层次结构来构造的方式进行对比。As you go through this sample, contrast this code with how it would be structured as an object hierarchy. 如果必须查询和控制的数据不是类层次结构,模式匹配可实现完善设计。When the data you must query and manipulate isn't a class hierarchy, pattern matching enables elegant designs.

我们不会从抽象形状定义以及添加不同的特定形状类开始,而是从每个几何形状的简单纯数据定义开始:Rather than starting with an abstract shape definition and adding different specific shape classes, let's start instead with simple data only definitions for each of the geometric shapes:

public class Square
{
    public double Side { get; }

    public Square(double side)
    {
        Side = side;
    }
}
public class Circle
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }
}
public struct Rectangle
{
    public double Length { get; }
    public double Height { get; }

    public Rectangle(double length, double height)
    {
        Length = length;
        Height = height;
    }
}
public class Triangle
{
    public double Base { get; }
    public double Height { get; }

    public Triangle(double @base, double height)
    {
        Base = @base;
        Height = height;
    }
}

在这些结构中,我们编写一个方法来计算某种形状的面积。From these structures, let's write a method that computes the area of some shape.

is 类型模式表达式The is type pattern expression

在 C# 7.0 之前,需要在一系列 ifis 语句中测试每种类型:Before C# 7.0, you'd need to test each type in a series of if and is statements:

public static double ComputeArea(object shape)
{
    if (shape is Square)
    {
        var s = (Square)shape;
        return s.Side * s.Side;
    } 
    else if (shape is Circle)
    {
        var c = (Circle)shape;
        return c.Radius * c.Radius * Math.PI;
    }
    // elided
    throw new ArgumentException(
        message: "shape is not a recognized shape",
        paramName: nameof(shape));
}

上面的代码是类型模式的经典表达式:测试变量以确定其类型并基于该类型执行不同操作。That code above is a classic expression of the type pattern: You're testing a variable to determine its type and taking a different action based on that type.

通过使用 is 表达式的扩展在测试成功时对变量赋值,此代码变得更加简单:This code becomes simpler using extensions to the is expression to assign a variable if the test succeeds:

public static double ComputeAreaModernIs(object shape)
{
    if (shape is Square s)
        return s.Side * s.Side;
    else if (shape is Circle c)
        return c.Radius * c.Radius * Math.PI;
    else if (shape is Rectangle r)
        return r.Height * r.Length;
    // elided
    throw new ArgumentException(
        message: "shape is not a recognized shape",
        paramName: nameof(shape));
}

在此更新的版本中,is 表达式会测试变量并将它分配给具有正确类型的新变量。In this updated version, the is expression both tests the variable and assigns it to a new variable of the proper type. 另请注意,此版本包含 Rectangle 类型(即 struct)。Also, notice that this version includes the Rectangle type, which is a struct. 新的 is 表达式使用值类型以及引用类型。The new is expression works with value types as well as reference types.

模式匹配表达式的语言规则可帮助避免误用匹配表达式的结果。Language rules for pattern matching expressions help you avoid misusing the results of a match expression. 在上面的示例中,仅当相应的模式匹配表达式具有 true 结果时,变量 scr 才处于范围内并进行明确赋值。In the example above, the variables s, c, and r are only in scope and definitely assigned when the respective pattern match expressions have true results. 如果尝试在另一个位置中使用任一变量,则代码会生成编译器错误。If you try to use either variable in another location, your code generates compiler errors.

让我们从范围开始,来详细了解一下这两个规则。Let's examine both of those rules in detail, beginning with scope. 变量 c 只处于第一个 if 语句的 else 分支的范围内。The variable c is in scope only in the else branch of the first if statement. 变量 s 处于方法 ComputeAreaModernIs 的范围内。The variable s is in scope in the method ComputeAreaModernIs. 这是因为 if 语句的每个分支都为变量建立单独的范围。That's because each branch of an if statement establishes a separate scope for variables. if 语句本身不会。However, the if statement itself doesn't. 这意味着在 if 语句中声明的变量会处于与 if 语句相同的范围中(本例中的方法。)此行为并不是模式匹配专有,而是变量作用域以及 ifelse 语句的已定义行为。That means variables declared in the if statement are in the same scope as the if statement (the method in this case.) This behavior isn't specific to pattern matching, but is the defined behavior for variable scopes and if and else statements.

由于在 true 时进行明确赋值这一机制,当相应 if 语句为 true 时,会对变量 cs 赋值。The variables c and s are assigned when the respective if statements are true because of the definitely assigned when true mechanism.

提示

本主题中的示例使用建议构造,其中的模式匹配 is 表达式在 if 语句的 true 分支中对匹配变量明确赋值。The samples in this topic use the recommended construct where a pattern match is expression definitely assigns the match variable in the true branch of the if statement. 可以通过编写 if (!(shape is Square s)) 来反转逻辑,变量 s 会只在 false 分支中进行明确赋值。You could reverse the logic by saying if (!(shape is Square s)) and the variable s would be definitely assigned only in the false branch. 虽然这是有效的 C#,不过不建议使用它,因为遵循该逻辑更令人困惑。While this is valid C#, it is not recommended because it is more confusing to follow the logic.

这些规则意味着,如果不符合模式,不可能意外访问模式匹配表达式的结果。These rules mean that you're unlikely to accidentally access the result of a pattern match expression when that pattern wasn't met.

使用模式匹配 switch 语句Using pattern matching switch statements

随着时间推移,可能需要支持其他形状类型。As time goes on, you may need to support other shape types. 随着要测试的条件数增加,你会发现使用 is 模式匹配表达式可能会变得很不方便。As the number of conditions you're testing grows, you'll find that using the is pattern matching expressions can become cumbersome. 除了需要对要检查的每种类型使用 if 语句,is 表达式仅限为测试输入是否与单个类型匹配。In addition to requiring if statements on each type you want to check, the is expressions are limited to testing if the input matches a single type. 在这种情况下,会发现 switch 模式匹配表达式会是更好的选择。In this case, you'll find that the switch pattern matching expressions becomes a better choice.

传统 switch 语句是模式表达式:支持常量模式。The traditional switch statement was a pattern expression: it supported the constant pattern. 可以将变量与 case 语句中使用的任何常量进行比较:You could compare a variable to any constant used in a case statement:

public static string GenerateMessage(params string[] parts)
{
    switch (parts.Length)
    {
        case 0:
            return "No elements to the input";
        case 1:
            return $"One element: {parts[0]}";
        case 2:
            return $"Two elements: {parts[0]}, {parts[1]}";
        default:
            return $"Many elements. Too many to write";
    }
}

switch 语句支持的唯一模式是常量模式。The only pattern supported by the switch statement was the constant pattern. 它进一步限制为数字类型和 string 类型。It was further limited to numeric types and the string type. 这些限制已移除,现在可以使用类型模式编写 switch 语句:Those restrictions have been removed, and you can now write a switch statement using the type pattern:

public static double ComputeAreaModernSwitch(object shape)
{
    switch (shape)
    {
        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Rectangle r:
            return r.Height * r.Length;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

模式匹配 switch 语句使用的语法对于使用过传统 C 样式 switch 语句的开发人员会比较熟悉。The pattern matching switch statement uses familiar syntax to developers who have used the traditional C-style switch statement. 会计算每个 case,并执行与输入变量匹配的条件下的代码。Each case is evaluated and the code beneath the condition that matches the input variable is executed. 代码执行无法从一个 case 表达式“贯穿”到下一个表达式;case 语句的语法要求,每个 case 都必须以 breakreturngoto 结尾。Code execution can't "fall through" from one case expression to the next; the syntax of the case statement requires that each case end with a break, return, or goto.

备注

用于跳转到其他标签的 goto 语句仅对常量模式(经典 switch 语句)有效。The goto statements to jump to another label are valid only for the constant pattern (the classic switch statement).

有一些用于控制 switch 语句的重要新规则。There are important new rules governing the switch statement. 针对 switch 表达式中的变量类型的限制已移除。The restrictions on the type of the variable in the switch expression have been removed. 任何类型(如此示例中的 object)都可以使用。Any type, such as object in this example, may be used. Case 表达式不再限制为常量值。The case expressions are no longer limited to constant values. 移除该限制意味着对 switch 部分重新排序可能会更改程序的行为。Removing that limitation means that reordering switch sections may change a program's behavior.

限制为常量值时,最多只有一个 case 标签可以与 switch 表达式的值匹配。When limited to constant values, no more than one case label could match the value of the switch expression. 将该规则与每个 switch 部分不得贯穿到下一个部分的规则相结合,这样 switch 部分便可以按任何顺序重新排列而不会影响行为。Combine that with the rule that every switch section must not fall through to the next section, and it followed that the switch sections could be rearranged in any order without affecting behavior. 现在,使用更加通用的 switch 表达式时,每个部分的顺序都非常重要。Now, with more generalized switch expressions, the order of each section matters. switch 表达式按文本顺序进行计算。The switch expressions are evaluated in textual order. 执行会转移到与 switch 表达式匹配的第一个 switch 匹配。Execution transfers to the first switch label that matches the switch expression.
仅当其他任何 case 标签都不匹配时,才会执行 default case。The default case will only be executed if no other case labels match. default case 最后一个进行计算(无论文本顺序如何)。The default case is evaluated last, regardless of its textual order. 如果没有任何 default case,且其他 case 语句都不匹配,便会在 switch 语句后的语句处继续代码执行。If there's no default case, and none of the other case statements match, execution continues at the statement following the switch statement. 不会执行任何 case 标签代码。None of the case labels code is executed.

case 表达式中的 when 语句when clauses in case expressions

可以通过对 case 标签使用 when 子句,为面积为 0 的那些形状创建特殊 case。You can make special cases for those shapes that have 0 area by using a when clause on the case label. 边长为 0 的正方形,或半径为 0 的圆形的面积为 0。A square with a side length of 0, or a circle with a radius of 0 has a 0 area. 可通过对 case 标签使用 when 语句来指定该条件:You specify that condition using a when clause on the case label:

public static double ComputeArea_Version3(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

此更改演示了有关新语法的几个要点。This change demonstrates a few important points about the new syntax. 首先,多个 case 标签可以应用于一个 switch 部分。First, multiple case labels can be applied to one switch section. 在其中任何标签为 true 时执行语句块。The statement block is executed when any of those labels is true. 在此例中,如果 switch 表达式是面积为 0 的圆形或正方形,则方法返回常量 0。In this instance, if the switch expression is either a circle or a square with 0 area, the method returns the constant 0.

此示例为第一个 switch 块在两个 case 标签中引入了两个不同的变量。This example introduces two different variables in the two case labels for the first switch block. 请注意,此 switch 块中的语句未使用变量 c(用于圆形)或 s(用于方形)。Notice that the statements in this switch block don't use either the variables c (for the circle) or s (for the square). 这两个变量在此 switch 块中都未明确赋值。Neither of those variables is definitely assigned in this switch block. 如果有任一 case 匹配,则对其中一个变量明确赋值。If either of these cases match, clearly one of the variables has been assigned. 不过,无法判断在编译时对哪个变量进行赋值,因为任一 case 在运行时都可能会匹配。However, it's impossible to tell which has been assigned at compile time, because either case could match at runtime. 因此,大多数情况下,如果将多个 case 标签用于同一个块,不会在 case 语句中引入新变量,或只在 when 子句中使用变量。For that reason, most times when you use multiple case labels for the same block, you won't introduce a new variable in the case statement, or you'll only use the variable in the when clause.

添加了这些面积为 0 的形状之后,我们再添加一些形状类型:一个矩形和一个三角形:Having added those shapes with 0 area, let's add a couple more shape types: a rectangle and a triangle:

public static double ComputeArea_Version4(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
        case Triangle t when t.Base == 0 || t.Height == 0:
        case Rectangle r when r.Length == 0 || r.Height == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Triangle t:
            return t.Base * t.Height / 2;
        case Rectangle r:
            return r.Length * r.Height;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

这组更改为每个新形状的退化情况、标签和块添加 case 标签。This set of changes adds case labels for the degenerate case, and labels and blocks for each of the new shapes.

最后,可以添加 null case 来确保参数不是 nullFinally, you can add a null case to ensure the argument isn't null:

public static double ComputeArea_Version5(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
        case Triangle t when t.Base == 0 || t.Height == 0:
        case Rectangle r when r.Length == 0 || r.Height == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Triangle t:
            return t.Base * t.Height / 2;
        case Rectangle r:
            return r.Length * r.Height;
        case null:
            throw new ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null");
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

null 模式的特殊行为十分有趣,因为模式中的常量 null 没有类型,但可以转换为任何引用类型或可以为 null 的类型。The special behavior for the null pattern is interesting because the constant null in the pattern doesn't have a type but can be converted to any reference type or nullable type. 语言定义 null 值不会匹配任何类型模式(无论变量的编译时类型如何),而不是将 null 转换为任何类型。Rather than convert a null to any type, the language defines that a null value won't match any type pattern, regardless of the compile-time type of the variable. 此行为使新的基于 switch 的类型模式与 is 语句保持一致:如果检查的值为 nullis 语句始终返回 falseThis behavior makes the new switch based type pattern consistent with the is statement: is statements always return false when the value being checked is null. 它也更简单;也就是说,在检查类型后,便无需执行其他 null 检查。It's also simpler: once you've checked the type, you don't need an additional null check. 可以从上面示例中的任何 case 块都没有 null 检查得知:此类检查不是必需的,因为与类型模式匹配即可保证非 null 值。You can see that from the fact that there are no null checks in any of the case blocks of the samples above: they aren't necessary, since matching the type pattern guarantees a non-null value.

case 表达式中的 var 声明var declarations in case expressions

引入 var 作为一种匹配表达式也为模式匹配引入了新规则。The introduction of var as one of the match expressions introduces new rules to the pattern match.

第一条规则是 var 声明遵循正常的类型推理规则:推理出类型是 switch 表达式的静态类型。The first rule is that the var declaration follows the normal type inference rules: The type is inferred to be the static type of the switch expression. 根据此规则,类型始终匹配。From that rule, the type always matches.

第二个规则是,var 声明没有其他类型模式表达式中包含的 null 检查。The second rule is that a var declaration doesn't have the null check that other type pattern expressions include. 也就是说,变量可为 NULL,只有在这种情况下,才必须执行 NULL 检查。That means the variable may be null, and a null check is necessary in that case.

这两个规则表示,在许多情况下,case 表达式中的 var 声明匹配与 default 表达式相同的条件。Those two rules mean that in many instances, a var declaration in a case expression matches the same conditions as a default expression. 因为任何非默认事例都优先于 default 事例,所以永远不会执行 default 事例。Because any non-default case is preferred to the default case, the default case will never execute.

备注

如果 default 事例已编写但永远不会执行,编译器也不会发出警告。The compiler does not emit a warning in those cases where a default case has been written but will never execute. 这与在已列出所有可能事例的情况下的当前 switch 语句行为一致。This is consistent with current switch statement behavior where all possible cases have been listed.

第三个规则引入了 var 事例可能适用的用途。The third rule introduces uses where a var case may be useful. 假设要进行模式匹配,其中输入是字符串,且要搜索已知命令值。Imagine that you're doing a pattern match where the input is a string and you're searching for known command values. 可以编写如下代码:You might write something like:

static object CreateShape(string shapeDescription)
{
    switch (shapeDescription)
    {
        case "circle":
            return new Circle(2);

        case "square":
            return new Square(4);
        
        case "large-circle":
            return new Circle(12);

        case var o when (o?.Trim().Length ?? 0) == 0:
            // white space
            return null;
        default:
            return "invalid shape description";
    }            
}

var 事例匹配 null、空字符串或任何仅包含空白符的字符串。The var case matches null, the empty string, or any string that contains only white space. 请注意,为了确保不会意外抛出 NullReferenceException,上面的代码使用 ?. 运算符。Notice that the preceding code uses the ?. operator to ensure that it doesn't accidentally throw a NullReferenceException. default case 处理此命令分析程序不理解的其他任何字符串值。The default case handles any other string values that aren't understood by this command parser.

在这个例子中,建议使用 var 事例表达式,而不是 default 表达式。This is one example where you may want to consider a var case expression that is distinct from a default expression.

结论Conclusions

使用模式匹配构造,可以轻松管理继承层次结构未关联的不同变量和类型之间的控制流。Pattern Matching constructs enable you to easily manage control flow among different variables and types that aren't related by an inheritance hierarchy. 还可以控制逻辑以使用对变量进行测试的任何条件。You can also control logic to use any condition you test on the variable. 这可实现在生成分布程度更高的应用程序(其中的数据和操作这些数据的方法是分开的)时更常需要的模式和惯例。It enables patterns and idioms that you'll need more often as you build more distributed applications, where data and the methods that manipulate that data are separate. 你会注意到,此示例中使用的形状结构不包含任何方法,只包含只读属性。You'll notice that the shape structs used in this sample don't contain any methods, just read-only properties. 模式匹配适用于任何数据类型。Pattern Matching works with any data type. 你编写了检查对象并基于这些条件进行控制流决策的表达式。You write expressions that examine the object, and make control flow decisions based on those conditions.

将此示例中的代码与通过为抽象 Shape 和特定派生类(各自具有自己的用于计算面积的虚方法实现)创建类层次结构而产生的设计进行比较。Compare the code from this sample with the design that would follow from creating a class hierarchy for an abstract Shape and specific derived shapes each with their own implementation of a virtual method to calculate the area. 你通常会发现,在处理数据并希望将数据存储问题与行为问题分开时,模式匹配表达式可能是非常有用的工具。You'll often find that pattern matching expressions can be a very useful tool when you're working with data and want to separate the data storage concerns from the behavior concerns.