C# 7 – Сопоставление с образцом (pattern matсhing)

C# logo Одно из самых интересных нововведений С# 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 образцы. В следующей, восьмой, версии языка этот список пополнится новыми вариантами.