模式比對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 型別,它是 structAlso, 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.

當個別的 if 陳述式為 true 時會指派變數 cs,因為 true 機制時會明確指派。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. 您可以說明只在 false 分支中明確指派 if (!(shape is Square s)) 和變數 s,以回復邏輯。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 都以 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. 可使用任何型別,例如本例的 objectAny 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.
只有無任何其他狀況標籤符合時才會執行 default 狀況。The default case will only be executed if no other case labels match. 無論其文字順序為何,default 狀況都是最後才評估。The default case is evaluated last, regardless of its textual order. 如果沒有任何 default 狀況,且無其他 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 的圖形建立特殊案例。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. 如果符合任一種狀況,顯然指派了其中一個變數。If either of these cases match, clearly one of the variables has been assigned. 不過,在編譯時期不可能分辨「哪個」已被指派,因為在執行階段任何一種狀況都可能符合。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 狀況,以確保引數不是 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 陳述式一致:要檢查的值是 null 時,is 陳述式一律會傳回 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. 您可以從上述範例的任何案例區塊中不會進行任何 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. 留意到上述程式碼會使用 ?. 運算子來確保它不會意外地擲回 NullReferenceExceptionNotice that the preceding code uses the ?. operator to ensure that it doesn't accidentally throw a NullReferenceException. default 狀況會處理此命令剖析器無法理解的任何其他字串值。The default case handles any other string values that aren't understood by this command parser.

這是一個範例,其中您可能想要考慮與 default 運算式不同的 var Case 運算式。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.