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

C# logoОдно из самых интересных нововведений С# 7 – это появление понятия образца (pattern) и операций сопоставления тестируемого значения с образцом (pattern matсhing). Их использование позволяет писать более компактный и, одновременно, удобный для чтения и понимания код. Разберемся подробнее с новой возможностью.

Образцы

С# 7 устанавливает несколько типов образцов:

  • Константа – позволяет определять соответствие тестируемого значения заданному. Формат: x, где x – значение константы.
  • Тип – проверяет имеет ли тестируемое значение указанный тип и создает переменную этого типа. Формат: T x, где T - заданный тип, а x - идентификатор новой переменной.
  • Переменная – копирует исходное значение в новую переменную. Формат: var x, где x - идентификатор новой переменной.

C# 7 поддерживает сопоставление с образцом для 2-х языковых конструкций: is и switch/case.

Разработчики языка обещают в будущем добавить новые виды образцов и поддержку других конструкций.

Поддержка сопоставлений в конструкции is

Использование is для сопоставления с образцом-константой

if (x is 5) { … }
if (y is null) { … }

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

object x = 5;
 
if (x is 5) { … } // OK. Результат сопоставления true.
if (x == 5) { … } // Ошибка: Оператор == не может применяться к операндам object и int.

Использование is для сопоставления с образцом-типом

Один из вариантов записи сопоставления известен разработчикам уже достаточно давно:

if (x is ISomeInterface) { ... }

Но теперь, случае успешного сравнения, можно сразу получить новую переменную заданного типа с присвоенным в нее исходным значением:

object x = 5;
if (x is int i) { Console.WriteLine(i); }

Результатом сопоставления будет значение true, при этом будет создана переменная i типа int со значением равным 5.

Кстати, переписать данный пример как int i = x as int; не получится.

Применение данного подхода позволяет создавать достаточно компактные выражения. Например, получим значение типа int из переменной scr типа object, предполагая что в ней находится значение или типа int или типа string:

if (src is int i || (src is string s && int.TryParse(s, out i)) {
    /* используем значение i */
}

Обратите внимание, что вызов TryParse(…) использует переменную s, которая была объявлена в операции сопоставления.

Использование is для сопоставления с образцом-переменной

Не смотря не то, что запись is с образцом-переменной является корректной, смысл в этом отсутствует. По сути это аналог операции присваивания. Результатом такого сопоставления всегда будет значение true и новая переменная с исходным значением:

if (x is var y) { /* Здесь x == y */ }

Поддержка сопоставлений в конструкции switch/case

Использование switch/case для сопостав��ения с образцом-константой и образцом-типом

C# 7 позволяет использовать образцы-типы и образцы-константы в качестве значений для 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 circle:
        // Используем circle типа ICircle
        break;
 
    default:
        // Действия по умолчанию (неизвестный тип)
        break;
 
    case null:
        // Отдельная обработка значения null
        throw new ArgumentNullException(nameof(shape));
}

Обратите внимание, что:

  • При использовании типов становится важен порядок case. Если в данном примере case IRectangle поставить выше case IBox, то код в case IBox никогда не сработает. Дело в том, что IBox является наследником IRectangle и может быть приведен к нему (поэтому для экземпляра IBox сработает case IReactange).
  • Специальная ветка default всегда обрабатывается последней, даже если после нее записаны другие варианты. В данном примере, сопоставление исходного значения с null будет выполнено раньше перехода к default.
  • В примере одновременно использованы как образцы-типы, так и образец-константа (null).
  • Если в значении, приведенном к заданному типу, нет необходимости, то можно использовать подстановку "_":
case ICircle _: // в этой ветке новой переменной создано не будет

Использование switch/case для сопоставления с образцом-переменой

В отличии от варианта с is, у использования образца-переменой с switch/case есть смысл. В следующем примере используется swtich для сопоставления как с константами, так и для проверки вхождения элемента в коллекцию (в реальном приложении коллекции могут создаваться по правилам бизнес логики или загружаться из источника данных):

int code = 142;
 
var collection = new int[] { 77, 42, 54 };
var collection2 = new int[] { 177, 142, 154 };
 
switch (code) {
    case 1:
        Console.WriteLine("Код равен 1");
        break;
 
    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;
}

По сути, это альтернатива записи вида if…else if … … else.

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

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

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

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