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

Комментарии (2) -

Ждём сопоставления на уровне F#/Nemerle.
Это конечно хорошо, что добавили, но для тех, кто пользовался другими языками это очень слабо.

NN Ну не все сразу. На мой взгляд новые возможности нужно вносить аккуратно, чтобы не испортить сам язык.

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