C# 8 – Новые образцы для сопоставлений (pattern matching)

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

Прежде всего стоит отметить, что все образцы доступны для использования в конструкциях is, switch/case и новом выражении switch.

Начиная с C# 7 были доступны три варианта образцов: образец константы, образец типа и образец переменной. А вот что добавилось в этот список с выходом С# 8:

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

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

Формат записи использует фигурные скобки:

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

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

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

Оба варианта записи можно использовать в одном образце. Рассмотрим пример c конструкцией is:

if (movie is { Title: null, Id: var i}) 
    throw new Exception($"Пустой заголовок у фильма с Id = {i}.");

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

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

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

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

Формат записи – указание имен позиций и контрольных значений, перечисленные в круглых скобках:

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

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

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

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

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

if (song is (_, string title, year: 2012))
    return $"Песня {title} из 2012 года.";

Образец из примера соответствует объектам, которые не равны null, а позиция year равна 2020. При этом первая позиция игнорируется и может иметь любое значение. Полученная из второй позиции переменная title используется для создания строки, которую вернет данный код.

Частный случай – образец кортежа (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()
    };
}

Образец константы - особые случаи

В C# 8 образец константы получил несколько специальных вариантов:

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

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

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

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

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

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

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

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

if (rec is Song { Title: null, Id: int i }) 
	throw new Exception($"Пустой заголовок у песни с Id = {i}.");

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

if (rec is Book(_, string title, int year))
    return $"Book {title}. Year: {year}",

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

Обобщающий пример

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

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

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

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

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

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

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

        // Образцы типа
        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}")
    };
}

Еще больше возможностей для сопоставлений появилось в C# 9.

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