Углублённое изучение

Pattern matching и switch в C# с примерами

Обновлено: · 5 мин чтения

Сопоставление с образцом (pattern matching) позволяет проверить форму значения и сразу достать из него нужные данные: тип, свойства, диапазон, элементы списка. Главные инструменты - оператор is, инструкция switch и выражение switch. Ниже разобраны все виды шаблонов, от типов и свойств до логических комбинаций and, or, not и шаблонов списков.

Pattern matching и switch в C# с примерами
Содержание
  1. Что даёт сопоставление с образцом
  2. Оператор is: проверка типа и значения
  3. Выражение switch
  4. Шаблон типа
  5. Шаблон свойств
  6. Реляционные шаблоны
  7. Логические шаблоны and, or, not
  8. Шаблоны списков
  9. Условие when и классический switch
  10. Типичные ошибки
  11. Частые вопросы

Что даёт сопоставление с образцом

Раньше проверка типа и приведение требовали нескольких строк: сначала is, затем явное приведение, потом проверка полей. Шаблоны объединяют это в одно выражение. Компилятор проверяет форму значения, а при совпадении создаёт переменную нужного типа, готовую к использованию.

Шаблоны работают в трёх местах: в операторе is, в инструкции switch и в выражении switch. Набор шаблонов везде одинаков, поэтому достаточно выучить его один раз.

Оператор is: проверка типа и значения

Самый простой шаблон - тип. Запись obj is string s проверяет тип и при успехе кладёт значение в переменную s. Переменная доступна только там, где компилятор уверен, что шаблон совпал.

object value = "привет";

if (value is string text && text.Length > 3)
    Console.WriteLine($"Строка длиной {text.Length}");

if (value is not null)
    Console.WriteLine("значение задано");

if (value is not int)
    Console.WriteLine("это не число");

Проверка на null через is null надёжнее, чем == null, потому что перегруженный оператор равенства её не обманет. Про типичные причины исключений при пустых ссылках есть отдельная статья о NullReferenceException.

Выражение switch

Выражение switch возвращает значение, поэтому его можно присвоить переменной или вернуть из метода. Каждая ветка записывается как образец => результат, ветки разделяются запятыми, а _ означает "любое остальное значение". Если ни одна ветка не подошла, выбрасывается SwitchExpressionException, поэтому ветку по умолчанию обычно добавляют.

static string DayType(DayOfWeek day) => day switch
{
    DayOfWeek.Saturday or DayOfWeek.Sunday => "выходной",
    _ => "рабочий день"
};

static int Score(string grade) => grade switch
{
    "A" => 5,
    "B" => 4,
    "C" => 3,
    _ => 0
};

Console.WriteLine(DayType(DayOfWeek.Sunday));   // выходной
Console.WriteLine(Score("B"));                  // 4

Отличия от классической инструкции switch: нет слов case и break, ветка всегда возвращает значение, а компилятор предупреждает, если не все варианты перечисления покрыты.

Шаблон типа

Шаблон типа сопоставляет значение с классом или интерфейсом. Это удобная замена цепочке if ... is ... else if. Порядок веток важен: сначала пишут более частные типы, затем общие, иначе общая ветка перекроет частную, и компилятор сообщит об ошибке.

abstract record Shape;
record Circle(double Radius) : Shape;
record Rectangle(double Width, double Height) : Shape;
record Triangle(double Base, double Height) : Shape;

static double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    Triangle t => 0.5 * t.Base * t.Height,
    null => throw new ArgumentNullException(nameof(shape)),
    _ => throw new NotSupportedException()
};

Console.WriteLine(Area(new Rectangle(3, 4)));   // 12

Если тип в ветке не нужен, переменную можно опустить: Circle => "круг". Это называется шаблоном типа без объявления, а ещё его можно записать как Circle _.

Шаблон свойств

Шаблон свойств проверяет значения свойств объекта в фигурных скобках. Проверять можно вложенные свойства через точку, что делает запись компактной.

record Address(string City, string Country);
record Customer(string Name, int Age, Address Address, bool IsVip);

static decimal DeliveryPrice(Customer c) => c switch
{
    { IsVip: true } => 0m,
    { Address.Country: "Россия", Address.City: "Москва" } => 200m,
    { Address.Country: "Россия" } => 450m,
    { Address: null } => 1000m,
    _ => 1500m
};

var customer = new Customer("Ирина", 29, new Address("Москва", "Россия"), false);
Console.WriteLine(DeliveryPrice(customer));   // 200

Если свойств много, можно сочетать проверку типа и свойств в одной ветке: Circle { Radius: 0 } => "вырожденный круг". Для записей и кортежей доступен позиционный шаблон, где значения сопоставляются по порядку компонентов.

static string Describe(Shape s) => s switch
{
    Circle { Radius: 0 } => "вырожденный круг",
    Rectangle(var w, var h) when w == h => "квадрат",
    Rectangle(var w, var h) => $"прямоугольник {w} на {h}",
    _ => "другая фигура"
};

static string Quadrant(int x, int y) => (x, y) switch
{
    (0, 0) => "начало координат",
    (> 0, > 0) => "первая четверть",
    (< 0, > 0) => "вторая четверть",
    (< 0, < 0) => "третья четверть",
    (> 0, < 0) => "четвёртая четверть",
    _ => "на оси"
};

Реляционные шаблоны

Реляционные шаблоны сравнивают значение через <, >, <= и >= с константой. Они заменяют длинные цепочки условий для диапазонов и работают с числами, символами и перечислениями.

static string Temperature(int degrees) => degrees switch
{
    < -20 => "лютый мороз",
    < 0 => "мороз",
    < 15 => "прохладно",
    < 25 => "тепло",
    _ => "жарко"
};

Console.WriteLine(Temperature(-5));    // мороз
Console.WriteLine(Temperature(30));    // жарко

Ветки проверяются сверху вниз, поэтому условие "меньше нуля" автоматически означает "не меньше -20". Это удобно, но требует внимания к порядку.

Логические шаблоны and, or, not

Логические комбинаторы объединяют шаблоны. and требует совпадения обоих, or - любого из двух, not инвертирует. Скобки группируют части. Обратите внимание: это ключевые слова, а не операторы && и ||, и применяются они к шаблонам, а не к булевым выражениям.

static bool IsLetterOrDigit(char c) =>
    c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or (>= '0' and <= '9');

static string AgeGroup(int age) => age switch
{
    < 0 or > 130 => "некорректный возраст",
    < 14 => "ребёнок",
    >= 14 and < 18 => "подросток",
    >= 18 and < 65 => "взрослый",
    _ => "пожилой"
};

object o = 5;
if (o is not null and int)
    Console.WriteLine("число");

string? name = null;
if (name is not (null or ""))
    Console.WriteLine(name);

Шаблоны списков

Начиная с C# 11 массивы и списки можно сопоставлять по структуре. Квадратные скобки описывают элементы по порядку, _ означает любой элемент, а шаблон среза .. заменяет любое количество элементов, в том числе ноль. Чтобы получить срез в переменную, пишут .. var rest.

static string Check(int[] numbers) => numbers switch
{
    [] => "пустой массив",
    [var single] => $"один элемент: {single}",
    [1, 2, 3] => "ровно 1, 2, 3",
    [var first, .., var last] when first == last => "первый равен последнему",
    [0, ..] => "начинается с нуля",
    [.., > 100] => "заканчивается числом больше 100",
    [_, _, ..] => "два элемента или больше"
};

Console.WriteLine(Check(new[] { 7 }));           // один элемент: 7
Console.WriteLine(Check(new[] { 4, 5, 4 }));     // первый равен последнему

static string Command(string[] args) => args switch
{
    ["start", var name] => $"запуск {name}",
    ["stop", ..] => "остановка",
    [] => "нет команд",
    _ => "неизвестная команда"
};

Шаблоны списков работают с любым типом, у которого есть Length или Count и индексатор. Для коллекций с поиском по ключу существуют другие структуры, например словарь Dictionary.

Условие when и классический switch

Ветку можно дополнить условием when, если нужно проверить то, что нельзя выразить шаблоном. Классическая инструкция switch получила те же шаблоны, и в ней тоже можно писать case int n when n > 0:. Для простого выбора действия инструкция по-прежнему подходит, а вот когда нужно вычислить значение, выражение switch короче.

static void Handle(object message)
{
    switch (message)
    {
        case int n when n > 100:
            Console.WriteLine("большое число");
            break;
        case int n:
            Console.WriteLine($"число {n}");
            break;
        case string { Length: 0 }:
            Console.WriteLine("пустая строка");
            break;
        case string s:
            Console.WriteLine($"строка {s}");
            break;
        case null:
            Console.WriteLine("null");
            break;
        default:
            Console.WriteLine("что-то иное");
            break;
    }
}

Если в проекте есть иерархия классов, удобно комбинировать шаблоны с наследованием, как в статье про работу с классами.

Типичные ошибки

  • Неверный порядок веток: общий шаблон стоит выше частного, и компилятор сообщает, что ветка недостижима.
  • Забытая ветка _ или default. В выражении switch это приводит к исключению в рантайме для незапланированных значений.
  • Попытка использовать переменную из шаблона вне области, где шаблон гарантированно совпал.
  • Путаница между or в шаблоне и || в условии. Запись x is 1 || 2 не скомпилируется, нужно x is 1 or 2.
  • Попытка сравнивать с неконстантой: в шаблоне можно использовать только константы, а не переменные. Для сравнения с переменной используйте when.
  • Слишком сложные ветки с пятью условиями: код становится нечитаемым, лучше вынести часть логики в метод.

Частые вопросы

Чем switch expression отличается от обычного switch?

Выражение возвращает значение и записывается короче, без case и break. Инструкция выполняет блоки кода и удобнее, когда в ветках несколько действий.

Что означает символ подчёркивания в ветке?

Это шаблон-отброс. Он совпадает с любым значением и обычно стоит в последней ветке как вариант по умолчанию.

С какой версии C# доступны шаблоны?

Базовые шаблоны появились в C# 7, реляционные и логические - в C# 9, шаблоны списков - в C# 11. Для всех примеров достаточно .NET 8.

Можно ли использовать pattern matching со строками?

Да, строки сопоставляются как константы, а в шаблоне свойств можно проверять длину. Сопоставление с регулярным выражением шаблонами не поддерживается.

Влияет ли switch expression на производительность?

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

Что будет, если ни одна ветка не подошла?

Выражение выбросит SwitchExpressionException. Компилятор заранее выдаёт предупреждение, если считает набор веток неполным.