Одно из самых интересных нововведений С# 7 – это появление понятия образца (pattern) и операций сопоставления тестируемого значения с образцом (pattern matсhing). Их использование позволяет писать более компактный и, одновременно, удобный для чтения и понимания код. |
С# 7 определяет несколько типов образцов: константа, тип и переменная. Любой их них может использоваться в сопоставлениях внутри конструкций is и switch.
Образец константы
Сопоставление с образцом константы позволят сравнить исходное значение с заданным.
Формат записи:
[значение константы]
Сразу перейдем к примерам:
if (x is 5) { … }
if (y is null) { … }
На первый взгляд может показаться что это аналог операции сравнения, но это не так. При сопоставлении учитывается реальный тип объекта, аналогично вызову Equals(…). Например:
object x = 5;
if (x is 5) { … } // OK. Результат сопоставления true.
if (x == 5) { … } // Ошибка: Оператор == не может применяться к операндам object и int.
Для switch запись с таким сопоставлением будет выглядеть следующем образом:
int? discountCode = GetDiscountCode();
switch (discountCode) {
case 1: return 5;
case 2: return 10;
case null: return 0;
default:
throw new ArgumentOutOfRangeException(nameof(discountCode));
}
В этом примере используются образцы константы для сравнения с 1, 2 и null. Как видно из примера, синтаксис в этом случае аналогичен синтаксису switch, использовавшемуся в предыдущих версиях.
Образец типа
Сопоставление с образцом типа проверяет тип исходного значения на возможность приведения его к заданному типу. В случае успешного сопоставления значение будет скопировано в переменную этого типа для дальнейших операций.
Формат записи:
[тип] [идентификатор новой переменной]
Если создавать новую переменную нет необходимости, то вместо ее идентификатора можно использовать подстановку _. А начиная с C# 9 это и вовсе стало не обязательным.
Проверка возможности приведения переменной к указанному типу существовала и в предыдущих версиях языка: if (x is ISomeInterface) { … }. После чего, как правило, внутри блока if значение x присваивалось в переменную этого типа.
Сопоставление с образцом типа использует схожую запись, но при этом позволяет сразу получить новую переменную заданного типа с присвоенным в нее исходным значением:
object x = 5;
if (x is int i) { Console.WriteLine(i); }
Результатом сопоставления x is int i будет значение true. При этом будет создана переменная i типа int, которой будет присвоено значение из исходной переменной x равное 5.
Такой синтаксис позволяет создавать достаточно компактные выражения. Например, получим значение типа int из переменной src, предполагая что в ней находится значение или типа int или типа string:
if (src is int i || (src is string s && int.TryParse(s, out i)) {
/* используем значение i */
}
Обратите внимание, что вызов TryParse(…) использует переменную s, которая была объявлена внутри сопоставления.
Образцы типа могут быть использованы в ветках case:
public interface IShape { }
public interface IRectangle : IShape { }
public interface IBox : IRectangle { }
public interface ICircle : IShape { }
…
IShape shape = GetShapeAt(x, y);
switch (shape) {
case IBox box:
// Используем box типа IBox.
break;
case IRectangle rect:
// Используем rect типа IRectangle.
break;
case ICircle _:
// Используется подстановка "_"
// чтобы не создавать новую переменную.
break;
default:
// Действия по умолчанию (неизвестный тип).
// Код default всегда выполняется после всех сопоставлений.
break;
case null:
// Отдельная обработка значения null
throw new ArgumentNullException(nameof(shape));
}
Поскольку сопоставление с образцом типа пытается привести исходное значение к заданному типу, то в конструкции switch типы-наследники должны быть указаны раньше их базовых типов. В противном случае, всегда будет срабатывать ветка case с базовым типом.
В частности, если в примере выше поменять местами блоки case IRectangle и case IBox, то код в case IBox никогда не сработает. Дело в том, что IBox является наследником IRectangle и может быть приведен к нему. В результате для экземпляра IBox всегда будет срабатывать сопоставление IRectangle rect и соответствующая ему ветка кода.
Кроме того, обратите внимание что специальная ветка default всегда обрабатывается последней, даже если после нее записаны другие варианты. В данном примере, сопоставление исходного значения с образцом-константой null будет выполнено раньше default.
Образец переменной
Сопоставление с образцом переменной копирует исходное значение в новую переменную и всегда считается успешным.
Формат записи:
var [идентификатор новой переменной]
Может показаться что от такого сопоставления мало толку. Но давайте посмотрим на следующий пример: проверка на вхождение элемента в коллекцию внутри конструкции switch:
int code = 142;
var collection = new int[] { 77, 42, 54 };
var collection2 = new int[] { 177, 142, 154 };
switch (code) {
case var c when collection.Contains(c):
Console.WriteLine($"Код из списка 1. Значение: {c}");
break;
case var c when collection2.Contains(c):
Console.WriteLine($"Код из списка 2. Значение: {c}");
break;
default:
Console.WriteLine($"Неизвестный код: {code}.");
break;
}
А вот в конструкции is рассматриваемое сопоставление особого смысла не имеет:
if (x is var y) { /* Здесь x == y */ }
Пользу из подобной записи можно извлечь только в случае типов, которые копируются по значению. В этом случае, можно модифицировать новое значение, оставит исходную переменную без изменений:
object x = 5;
if (x is 5 && x is var s) {
// операции которые могут изменить значение s
}
// x по прежнему 5
Особенность использования сопоставлений
Необходимо отметить одну особенность использования сопоставлений с образцом. Предположим, что есть класс Point, у которого есть свойство X. На первый взгляд может показаться что следующие выражения идиентичны:
if (point.X == 1) { … }
if (point is { X: 1 }) { … }
Однако, есть существенная разница в поведении, если переменная point равна null.
- Первый вариант выбросит исключение NullReferenceException при обращении к свойству X.
- А вот образец свойства представляет собой не пустой объект. Как результат, сопоставление значения null с таким образцом будет равно false и приложение просто продолжит выполнение. Это стоит учитывать при использовании сопоставлений.
Это все доступные для использования в C# 7 образцы. В следующей, восьмой, версии языка этот список пополнится новыми вариантами.