Операция сопоставления с образцом (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.