Сопоставление шаблонов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. Для проверки значений можно написать операторы if и switch.You write if or switch statements that test values. Если операторы совпадают, данные из этого значения извлекаются и используются.Then, when those statements match, you extract and use information from that value. Новые элементы синтаксиса — это расширения для операторов is и switch, с которыми вы уже знакомы.The 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 и switch, тестирующие значение переменной,You 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.

Выражение шаблона для типа isThe is type pattern expression

До выхода C# 7.0 каждый тип необходимо было тестировать в ряде операторов if и is: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.

Правила языка для выражений сопоставления шаблонов защищают от неправильного использования результатов примененного выражения match.Language rules for pattern matching expressions help you avoid misusing the results of a match expression. В приведенном выше примере переменные s, c и r находятся в области действия и назначаются явно, только если соответствующие выражения сопоставления шаблонов содержат результат true.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 находится в области действия только в ветви else первого оператора if.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 (в данном случае это метод).That means variables declared in the if statement are in the same scope as the if statement (the method in this case). Такое поведение не связано с сопоставлением шаблонов. Оно определено для области действия переменных и операторов if и else.This behavior isn't specific to pattern matching, but is the defined behavior for variable scopes and if and else statements.

Переменные c и s назначаются, если соответствующие операторы if имеют значение true, поскольку явно назначаются при использовании механизма true.The variables c and s are assigned when the respective if statements are true because of the definitely assigned when true mechanism.

Совет

Примеры в этом разделе включают рекомендуемую конструкцию, в которой выражение сопоставления шаблонов is явно назначает переменную сопоставления в ветви true оператора if.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.

Использование операторов сопоставления шаблонов switchUsing 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 всегда оканчивался на break, return или goto.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.

При ограничении постоянными значениями значению выражения switch может соответствовать только одна метка case.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.

Предложения when в выражениях casewhen clauses in case expressions

Для фигур с нулевой площадью можно создать специальные варианты, вставив предложение when в метку case.You can make special cases for those shapes that have 0 area by using a when clause on the case label. Квадрат с длиной стороны 0 или круг с радиусом 0 имеют нулевую площадь.A square with a side length of 0, or a circle with a radius of 0 has a 0 area. Это условие задается с помощью предложения when в метке case: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. Во-первых, к одному разделу switch можно применить сразу несколько меток case.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.In this instance, if the switch expression is either a circle or a square with 0 area, the method returns the constant 0.

В этом примере представлены две различные переменные в двух метках case для первого блока switch.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.

Теперь, когда мы добавили формы с нулевой площадью, добавим еще пару типов фигур — прямоугольник и треугольник: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, чтобы в качестве аргумента не использовался null:Finally, 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 value 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: операторы is всегда возвращают значение false, если проверяемое значение равно null.This 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.

Объявления var в выражениях casevar declarations in case expressions

Вследствие введения var в качестве одного из выражений сопоставления начинают действовать новые правила в отношении сопоставления шаблонов.The introduction of var as one of the match expressions introduces new rules to the pattern match.

Первое правило заключается в том, что объявление var подчиняется обычным правилам определения типа: тип определяется как статический тип выражения выбора вариантов.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.

Из этих двух правил следует, что во многих случаях объявление var в выражении case соответствует тем же условиям, что и выражение 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 обрабатывает все остальные строковые значения, которые не распознает этот анализатор команд.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.

См. такжеSee also