Новые возможности в C# 8 – Сопоставление с образцом в switch

C# logo Операция сопоставления с образцом (pattern matching) и её поддержка в конструкции switch появилась еще в C# 7. В восьмой версии языка не только расширился список образцов, доступных для использования разработчиками, но и изменилась форма записи switch. Рассмотрим обновленный синтаксис конструкции и варианты образцов.

Конструкция switch

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

[source] switch 
{
    [pattern 1] (var 1) (when [conditions])  => [expression 1],
    [pattern 2] (var 2) (when [conditions])  => [expression 2],
    …    
    [pattern N] (var N) (when [conditions])  => [expression N]
}

, где:

  • Обязательные параметры:
    • source – переменная, которая будет сопоставлена с образцами.
    • pattern 1pattern N – образцы для сопоставлений.
    • expression 1expression N – выражения, которые будут выполнены при удачном сопоставлении source с соответствующим образцом. Результаты всех выражений должны быть одного типа или приводиться к одному общему типу. Именно этот тип и будет типом результата, который вернет  
  • Опциональные:
    • var 1 … var N – переменная, тип которой совпадает с типом source, но может быть уточнен образцом. В случае успешного сопоставления в нее будет присвоено значение из source. Область видимости – выражение, соответствующее образцу.
    • when [contitions] – ключевое слово when позволяет определить дополнительные условия (contitions) для выполнения соответствующего выражения.

Старый вариант записи switch/case по прежнему можно использовать когда это необходимо.

Можно выделить следующие отличия нового варианта записи от предыдущего:

  • Блок switch может вернуть значение (им будет результат выполнения выражения), тип которого определяется выражениями.
  • Переменная для сопоставления указывается перед ключевым словом switch.
  • Ключевое слово case отсутствует.
  • Для выполнения действий используются выражения.
  • Образец отделяется от выражения при помощи символов "=>".
  • В качестве разделителя после выражения применяется запятая.

Для выполнения действий в switch используются однострочные выражения. Это позволяет конструкции сохранить легкость для чтения и понимания. Если в каком-то случае необходимо выполнить более одного действия, то их можно записать в виде локального метода или метода класса.

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

Варианты образцов (patterns)

Образец типа

Данный образец используется для проверки типа сопоставляемого значения. Формат записи:

[тип]

При выборе очередности сопоставлений необходимо учитывать что значения объектов могут быт�� приведены к их базовым классам (при наличии таковых).

Примеры

Book book => this.GetBookInfo(book),

Если значение может быть приведено к типу Book, то оно будет сохранено в переменной book с последующей передачей в метод класса GetBookInfo().

Movie _ => throw new NotSupportedException("Movies are not supported."),

Образец проверяет возможность приведения значения к типу Move. Если это возможно, то будет выброшено исключение.

Т.к. в самом значении нет необходимости, то вместо создания переменной используется подстановка "_".

Образец свойства

Образец позволяет сравнивать и получать значения конкретных свойств объекта. При этом из них достаточно указать только необходимые.  Формат записи сравнения:

{ [имя свойства 1]: [значение 1], … [имя свойства N]: [значение N] }

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

{ [имя свойства 1]: [тип переменной 1 / var] [имя переменной 1], … [имя свойства N]: [тип переменной N / var] [имя переменной N] }

Оба варианта записи можно использовать в одном образце одновременно.

Пример

{ Title: null, Id: int i } => throw new Exception($"Invalid record {i}."),

Данный код выбросит исключение, содержащие Id объекта в тексте, если свойство Title равно null.

Позиционный образец

Если объект поддерживает деконструкцию (у него есть метод Deconstruct), то её можно использовать для проверки и получения значений отдельных свойств. Формат записи для сравнения подразумевает указание имен позиций и контрольных значений, перечисленных в круглых скобках:

( [имя позиции 1]: [значение 1], … [имя позиции N] [значение N] )

Область видимости переменных – соответствующее выражение.

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

( [тип переменной 1] [имя переменной 1], …[тип переменной N]: [имя переменной N] )

Как и в случае со свойствами, оба варианта можно сочетать в одном образце.

Примеры

(int id, string title, year: 2012) => $"It's from 2012! {id}. Title: {title}",

Образец соответствует объектам, у которых позиция year равна 2000. Полученные при деконструкции переменные id и title используются для создания строки, которую вернет выражение.

(int id, string title, int year) => $"{id}. Title: {title}, Year: {year}",

В данном случае, объект, не равный null, будет деконструирован в 3 переменные.

Данный тип образца требует указание всех позиций. Однако, ненужные в конкретной ситуации позиции можно пропустить, используя подстановку "_" (нижнее подчеркивание).

Образец кортежа (tuple)

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

Например, следующий код определяет новое состояние замка в зависимости от текущего состояния и операции:

public State GetNewState(State current, Transition transition, bool hasKey)
{
    return (current, transition, hasKey) switch
    {
        (State.Opened, Transition.Close, _) => State.Closed,
        (State.Closed, Transition.Open, _) => State.Opened,
        (State.Closed, Transition.Lock, true) => State.Locked,
        (State.Locked, Transition.Unlock, true) => State.Closed,
        _ => throw new InvalidOperationException()
    };
}

Особые образцы

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

  • { } – образец произвольного объекта, который не равен null.
  • null – образец объекта, который равен null.
  • _ (нижнее подчеркивание) – образец любого объекта, равного или не равного null.

Вложенные образцы

Образцы можно комбинировать, вкладывая один в другой. Формат записи:

[Образец 1] [Образец 2] … [Образец N]

Такая запись подразумевает что Образец 2 вложен в Образец 1, Образец 3 в Образец 2 и т.д. При выполнении кода, сначала будет произведено сопоставление с Образцом 1. Если оно окажется успешным, то сопоставление перейдет к Образцу 2 и т.д. Соответствующее выражение выполнится если сопоставления со всеми образцами окажутся успешными.

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

Примеры

Song { Title: null, Id: int i } => throw new Exception($"Invalid song record {i}."),

Здесь использованы образцы типов и свойств. Есть значение можно привести к типу Song, а свойство Title у него равно null, то будет выброшено исключение.

Book (_, string title, int year) => $"Book {title}. Year: {year}",

В этом примере первый образец указывает на тип значения – Book. А позиционный образец используется для получения значений свойств объекта с последующей передачей их в выражение.

Пример

В завершении рассмотрим описанные выше возможности на простом абстрактном примере. Его цель - показать вариант записи образов.

Предположим, что существует базовый абстрактный класс RecordBase, от которого унаследованы классы Song, Book и Movie. Классы предназначены для хранения информации о песнях, книгах и фильмах соответственно. Следующий код использует блок switch чтобы вернуть отформатированную строку с данными о произведении. Обратите внимание, что в конце конструкции используется два образца: "{ }" и "null". Вместе они по действию эквивалентны образцу "_", но позволяют разделить обработку пустых и неопознанных значений.

public string Format(RecordBase record)
{
    return record switch
    {
        // Образец типа и условие 
        RecordBase r when r.UpdateDate > DateTime.Today => throw new Exception("Invalid record date."),

        // Позиционные образцы 
        (int id, string title, year: 2012) => $"It's from 2012! {id}. Title: {title}",
        (int id, string title, int year) => $"{id}. Title: {title}, Year: {year}",

        // Образец типа и образец свойств
        Song { Title: null, Id: int i } => throw new Exception($"Invalid song record {i}."),

        // Образец свойств
        { Title: null, Id: int i } => throw new Exception($"Invalid record {i}."),

        // Образец типа и позиционный образец
        Book (_, string title, int year) => $"Book {title}. Year: {year}",

        // Образцы типа
        Book b => this.GetBookInfo(b),
        Movie _ => throw new NotSupportedException("Movies are not supported yet."),

        // Образцы для обработки по умолчанию
        null => throw new NullReferenceException(),   
        { } obj => throw new Exception($"Unknown record type: {obj.GetType().FullName}")
    };
}

Добавить комментарий