Общие сведения о сопоставлении шаблонов

Сопоставление шаблонов — это метод, с помощью которого можно проверить выражение, чтобы определить, имеет ли оно определенные характеристики. Сопоставление шаблонов C# предоставляет более краткий синтаксис для тестирования выражений и выполнения действий при совпадении выражений. is Выражение поддерживает сопоставление шаблонов для проверки выражения и условно объявлять новую переменную в результат этого выражения. Выражение switchпозволяет выполнять действия на основе первого совпадающего шаблона для выражения. Эти два выражения поддерживают богатый словарь шаблонов.

В этой статье приводятся общие сведения о сценариях, в которых можно использовать сопоставление шаблонов. Эти методы могут повысить удобочитаемость и правильность кода. Полное описание всех шаблонов, которые можно применить, см. в статье о шаблонах в справочнике по языку.

Проверки значений NULL

Один из наиболее распространенных сценариев сопоставления шаблонов заключается в том, чтобы гарантировать, что значения не равны null. Можно проверить и преобразовать тип значения, допускающего значение NULL, в его базовый тип при тестировании на равенство null с помощью следующего примера:

int? maybe = 12;

if (maybe is int number)
{
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
}
else
{
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
}

Приведенный выше код является шаблоном объявления для проверки типа переменной и присвоения его новой переменной. Правила языка делают этот метод более надежным, чем многие другие. Переменная number доступна и назначается только в части предложения if, содержащей значение TRUE. При попытке доступа к ней где-либо еще, в предложении else либо после блока if, компилятор выдает ошибку. Во-вторых, поскольку оператор == не используется, этот шаблон работает, когда тип перегружает оператор ==. Это делает его идеальным способом проверки эталонных значений NULL с помощью добавления шаблона not:

string? message = ReadMessageOrDefault();

if (message is not null)
{
    Console.WriteLine(message);
}

В предыдущем примере для сравнения переменной с null использовался шаблон константы. not — это логический шаблон, для которого выполняется условие соответствия, если шаблон с отрицанием не соответствует выражению.

Проверки типов

Другим распространенным применением сопоставления шаблонов является проверка переменной на соответствие заданному типу. Например, следующий код проверяет, имеет ли переменная значение, отличное от NULL, и реализует интерфейс System.Collections.Generic.IList<T>. Если это так, он использует ICollection<T>.Count свойство в этом списке для поиска среднего индекса. Шаблон объявления не соответствует значению null независимо от типа переменной во время компиляции. Приведенный ниже код защищает от null, а также от типа, который не реализует IList.

public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
    {
        return list[list.Count / 2];
    }
    else if (sequence is null)
    {
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    }
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}

Одни и те же тесты можно применять в выражении switch для проверки переменной по отношению к нескольким различным типам. Эти сведения можно использовать для создания более эффективных алгоритмов на основе конкретного типа времени выполнения.

Сравнение дискретных значений

Можно также проверить переменную, чтобы найти совпадение по конкретным значениям. В следующем примеров показан код, с помощью которого проверяется значение по отношению ко всем возможным значениям, объявленным в перечислении:

public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };

В предыдущем примере демонстрируется метод диспетчеризации, основанный на значении перечисления. Последний случай _ — это шаблон отмены, который соответствует всем значениям. Он обрабатывает любые условия ошибок, в которых значение не соответствует одному из определенных значений enum. Если вы опустите этот переключатель, компилятор предупреждает, что выражение шаблона не обрабатывает все возможные входные значения. Во время выполнения выражение switch создает исключение, если проверяемый объект не соответствует ни одной из сторон выражения switch. Вместо набора значений перечисления можно использовать числовые константы. Подобную методику также можно использовать для постоянных строковых значений, представляющих команды:

public State PerformOperation(string command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

В предыдущем примере показан тот же алгоритм, но вместо перечисления в нем используются строковые значения. Этот сценарий следует использовать, если ваше приложение реагирует на текстовые команды, а не на обычный формат данных. Начиная с C# 11, можно также использовать Span<char> или ReadOnlySpan<char>тестировать для константных строковых значений, как показано в следующем примере:

public State PerformOperation(ReadOnlySpan<char> command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

Во всех этих примерах шаблон отмены гарантирует обработку всех входных данных. Компилятор помогает убедиться, что каждое возможное входное значение обработано.

Реляционные шаблоны

Реляционные шаблоны можно использовать для проверки того, как значение сравнивается с константами. Например, следующий код возвращает состояние воды на основе температуры по Фаренгейту:

string WaterState(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        (> 32) and (< 212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid / gas transition",
    };

Приведенный выше код также демонстрирует логический шаблон конъюнктивного выражения для проверка соответствия обоим реляционнымand шаблонам. Можно также использовать дизъюнктивный шаблон or для проверки соответствия любого из шаблонов. Два реляционных шаблона заключены в круглые скобки, которые можно использовать вокруг любого шаблона для ясности. Две последние стороны выражения switch обрабатывают случаи для точки замерзания и точки кипения. Без этих двух сторон компилятор выдает предупреждение о том, что логика не охватывает все возможные входные данные.

Предыдущий код также демонстрирует еще одну важную функцию компилятора для выражений сопоставления шаблонов: компилятор предупреждает вас, если вы не обрабатываете каждое входное значение. Компилятор также выдает предупреждение, если шаблон для переключения охватывается предыдущим шаблоном. Это дает вам свободу рефакторинга и переупорядочения выражений переключения выражений. Другой способ записи того же выражения может быть следующим:

string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",
};

Ключевой урок, приведенный в предыдущем примере, и любой другой рефакторинг или переупорядочение, заключается в том, что компилятор проверяет, обрабатывает ли код все возможные входные данные.

Несколько входных данных

Все шаблоны, описанные до сих пор, были проверка один вход. Можно написать шаблоны, которые проверяют несколько свойств объекта. Рассмотрим следующую запись Order:

public record Order(int Items, decimal Cost);

Предыдущий тип позиционной записи объявляет два элемента в явных позициях. Первым идет Items (элементы), а затем — Cost (стоимость) заказа. Дополнительные сведения см. в статье Записи.

Следующий код проверяет количество элементов и значение заказа для вычисления цены со скидкой:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Первые две стороны выражения анализируют два свойства Order. Третья анализирует только стоимость. Следующая проверяет на равенство null, а последняя сопоставляет любое другое значение. Если тип Order определяет подходящий метод Deconstruct, можно опустить имена свойств из шаблона и использовать деконструкцию для проверки свойств:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

В приведенном выше коде показан позиционный шаблон, в котором свойства деконструированы для выражения.

Шаблоны списков

Элементы в списке или массиве можно проверка с помощью шаблона списка. Шаблон списка предоставляет средства для применения шаблона к любому элементу последовательности. Кроме того, можно применить шаблон dis карта (_) для сопоставления любого элемента или применить шаблон среза, чтобы соответствовать нулю или нескольким элементам.

Шаблоны списка являются ценным инструментом, если данные не соответствуют обычной структуре. Вы можете использовать сопоставление шаблонов для проверки фигуры и значений данных, а не преобразования их в набор объектов.

Рассмотрим следующий фрагмент из текстового файла, содержащего банковские транзакции:

04-01-2020, DEPOSIT,    Initial deposit,            2250.00
04-15-2020, DEPOSIT,    Refund,                      125.65
04-18-2020, DEPOSIT,    Paycheck,                    825.65
04-22-2020, WITHDRAWAL, Debit,           Groceries,  255.73
05-01-2020, WITHDRAWAL, #1102,           Rent, apt, 2100.00
05-02-2020, INTEREST,                                  0.65
05-07-2020, WITHDRAWAL, Debit,           Movies,      12.57
04-15-2020, FEE,                                       5.55

Это формат CSV, но некоторые строки имеют больше столбцов, чем другие. Еще хуже для обработки один столбец в WITHDRAWAL типе содержит созданный пользователем текст и может содержать запятую в тексте. Шаблон списка, включающий шаблон dis карта, постоянный шаблон и шаблон var для записи данных о значении в этом формате:

decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
{
    balance += transaction switch
    {
        [_, "DEPOSIT", _, var amount]     => decimal.Parse(amount),
        [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
        [_, "INTEREST", var amount]       => decimal.Parse(amount),
        [_, "FEE", var fee]               => -decimal.Parse(fee),
        _                                 => throw new InvalidOperationException($"Record {string.Join(", ", transaction)} is not in the expected format!"),
    };
    Console.WriteLine($"Record: {string.Join(", ", transaction)}, New balance: {balance:C}");
}

В предыдущем примере принимается строковый массив, где каждый элемент является одним полем в строке. Ключи switch выражения во втором поле, определяющее тип транзакции, и количество оставшихся столбцов. Каждая строка гарантирует правильность формата данных. Шаблон dis карта (_) пропускает первое поле с датой транзакции. Второе поле соответствует типу транзакции. Оставшиеся совпадения элементов пропускают поле с суммой. Окончательное совпадение использует шаблон var для записи строкового представления суммы. Выражение вычисляет сумму для добавления или вычитания из баланса.

Шаблоны списка позволяют сопоставлять фигуру последовательности элементов данных. Вы используете шаблоны dis карта и срезов, чтобы соответствовать расположению элементов. Другие шаблоны используются для сопоставления характеристик отдельных элементов.

В этой статье представлен обзор видов кода, который можно написать с помощью сопоставления шаблонов в C#. В следующих статьях приведены дополнительные примеры использования шаблонов в сценариях и полный словарь шаблонов, доступных для использования.

См. также