Menu

Сопоставление с образцом в C#: is, шаблоны в switch, шаблоны свойств и списков

Как работает сопоставление с образцом (pattern matching) в C#: оператор is с шаблонами типа и констант, операторы switch с шаблонами в case и when, switch-выражения, шаблоны свойств, кортежей и позиционные, реляционные и логические шаблоны (and, or, not), шаблоны списков и в какой версии C# появился каждый из них.

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

Сопоставление с образцом (pattern matching) проверяет значение на соответствие форме: «это Circle?», «это null?», «его Status равен "paid", а Total больше 100?». Когда проверка успешна, шаблон может ещё и извлечь части значения в новые переменные. Возможность начиналась скромно в C# 7 с оператора is и росла в каждой следующей версии, поэтому на этой странице указана версия каждой формы.

Шаблон типа с is (C# 7)

До C# 7 проверка типа и его использование занимали два шага: as плюс проверка на null или is плюс приведение. Шаблон типа делает и то и другое сразу и даёт вам типизированную переменную:

Вывод:

card ending 4242: 49.90
transfer from PT50...: 1200
no payment
other payment: 5

p is CardPayment card истинно, только когда p это CardPayment (или производный от него класс), отличный от null, и тогда card хранит его с правильным статическим типом. Значение null никогда не совпадает с шаблоном типа, поэтому платёж null проваливается до проверки на null.

Область видимости и определённое присваивание

Переменная шаблона существует в объемлющем блоке, но компилятор позволяет читать её только там, где совпадение заведомо успешно. Благодаря этому работает форма раннего выхода, когда вы выходите пораньше, а переменную используете потом:

Вывод:

19.99
42 is over 40
price must be text

boxed is int n && n > 40 показывает другую распространённую форму: переменная доступна справа от &&, потому что эта сторона выполняется, только если совпадение успешно. Справа от || она не присвоена, и компилятор об этом говорит (CS0165).

Шаблоны также распаковывают: boxed is int n успешно для упакованного int и даёт значение без приведения, которое могло бы выбросить исключение. Для типов значений, допускающих null, maybe is int n успешно ровно тогда, когда у maybe есть значение.

Шаблоны констант и null

Константа тоже является шаблоном. x is null используется чаще всего, а C# 7 также разрешает x is 0, status is "paid" или член перечисления:

if (customer is null) return;          // null check that ignores any == overload
if (count is 0) Console.WriteLine("empty");
if (level is LogLevel.Error) Alert();

Многие команды предпочитают is null вместо == null, потому что его нельзя перенаправить перегрузкой оператора у типа. C# 9 добавляет отрицательную форму is not null.

Шаблоны в операторах switch (C# 7)

Те же шаблоны типа могут служить метками case, а условие when добавляет проверку. Case проверяются сверху вниз, поэтому упорядочивайте их от конкретных к общим:

static string Describe(object o)
{
    switch (o)
    {
        case null:                           return "nothing";
        case int n when n < 0:               return "negative number";
        case int n:                          return $"number {n}";
        case string s when s.Length == 0:    return "empty text";
        case string s:                       return $"text of {s.Length} chars";
        case IEnumerable<int> list:          return "a sequence of numbers";
        default:                             return o.GetType().Name;
    }
}

Describe(-4);                 // negative number
Describe("");                 // empty text
Describe(new List<int>());    // a sequence of numbers
Describe(2.5);                // Double

Компилятор проверяет порядок: case, который никогда не может совпасть, потому что его покрывает предыдущий, это ошибка CS8120. default всегда проверяется последним, где бы он ни стоял. Та же логика, записанная цепочками if/is, как в примере с платежами выше, работает в любой версии C# начиная с 7.

Switch-выражения (C# 8)

Switch-выражение это компактная форма для «вычислить одно значение из нескольких случаев». Сначала значение, затем switch, затем ветви pattern => result через запятую, с _ для всех остальных:

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

static double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle { Width: var w, Height: var h } => w * h,
    _ => throw new ArgumentException("Unknown shape", nameof(shape)),
};

Area(new Rectangle(3, 4));   // 12

Если во время выполнения не совпала ни одна ветвь, выбрасывается SwitchExpressionException, а компилятор предупреждает (CS8509), когда видит, что какой-то вход не обработан. Синтаксис описан на отдельной странице о switch-выражении; остальная часть этой страницы о шаблонах, которые можно ставить в ветви.

Шаблоны свойств (C# 8, расширены в C# 10)

Шаблон свойства сопоставляет свойства объекта с вложенными шаблонами в фигурных скобках. Объект при этом должен быть не null, поэтому один { } означает «не null»:

public record Address(string City, string Country);
public record Order(decimal Total, string Status, Address ShipTo, int Items);

static string Shipping(Order order) => order switch
{
    { Status: "cancelled" }                   => "no shipment",
    { ShipTo.Country: "PT", Total: >= 50m }   => "free, domestic",     // C# 10 dotted form
    { ShipTo.Country: "PT" }                  => "4.90, domestic",
    { Total: > 200m }                         => "free, international",
    _                                         => "12.00, international",
};

Shipping(new Order(60m, "paid", new Address("Porto", "PT"), 2));    // free, domestic
Shipping(new Order(260m, "paid", new Address("Lyon", "FR"), 2));    // free, international

В C# 8 и 9 обязательна вложенная форма: { ShipTo: { Country: "PT" } }. C# 10 разрешает сокращение через точку, использованное выше. Шаблоны свойств работают и с is, благодаря чему проверки нескольких условий читаются как описание: if (order is { Status: "paid", Items: > 0 }).

Реляционные и логические шаблоны (C# 9)

C# 9 добавил сравнения (<, <=, >, >=) и комбинаторы and, or и not, которые превращают диапазоны в шаблоны:

static string Grade(int score) => score switch
{
    < 0 or > 100      => "invalid",
    >= 90             => "A",
    >= 75 and < 90    => "B",
    >= 50             => "C",
    _                 => "F",
};

Grade(82);    // B
Grade(101);   // invalid

if (input is not null and not "") { ... }
if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z') { ... }   // and binds tighter than or

Эквивалент на C# 7 это цепочка if/else if или switch с условиями when (case int s when s >= 90:).

Шаблоны кортежей и позиционные шаблоны (C# 8)

Switch сразу по нескольким значениям делается через switch по кортежу:

static string Quadrant(int x, int y) => (x, y) switch
{
    (0, 0)       => "origin",
    (> 0, > 0)   => "I",
    (< 0, > 0)   => "II",
    (< 0, < 0)   => "III",
    (> 0, < 0)   => "IV",
    _            => "on an axis",
};

Quadrant(-2, 5);   // II

Позиционный шаблон делает то же для любого типа с методом Deconstruct, включая записи: case Point(0, 0): или p is Rectangle(var w, var h) && w == h.

Шаблоны списков (C# 11)

Шаблоны списков сопоставляют массивы и списки по их элементам. .. совпадает с любым числом элементов («срез»), а в позиции каждого элемента может стоять любой другой шаблон:

static string Route(string[] args) => args switch
{
    []                         => "help",
    ["add", var item]          => $"add {item}",
    ["remove", var item, ..]   => $"remove {item}",
    [var cmd, ..]              => $"unknown command {cmd}",
};

Route(new[] { "add", "milk" });             // add milk
Route(new[] { "remove", "milk", "now" });   // remove milk

int[] nums = { 1, 2, 3, 4 };
if (nums is [1, .., var last]) Console.WriteLine(last);   // 4

В какой версии что появилось

ШаблонПримерВерсия
Шаблон типа с переменнойo is Circle cC# 7.0
Шаблон константыx is null, n is 0C# 7.0
Шаблон varx is var vC# 7.0
Шаблоны в метках case, условия whencase int n when n < 0:C# 7.0
Switch-выражение, отбрасывание _x switch { ... }C# 8
Шаблон свойства{ Status: "paid" }C# 8
Шаблон кортежа(x, y) switch { (0, 0) => ... }C# 8
Позиционный шаблонPoint(0, 0)C# 8
Реляционный шаблон> 100, <= 0C# 9
Логические шаблоныnot null, >= 1 and <= 5, 'a' or 'b'C# 9
Голый шаблон типаcase Circle:, Circle => ...C# 9
Расширенный шаблон свойства{ ShipTo.City: "Porto" }C# 10
Шаблон списка и срез[first, .., last]C# 11

Частые ошибки

  • Порядок case от общих к конкретным. case Payment p: перед case CardPayment c: делает второй недостижимым (CS8120). Сначала конкретные.
  • Ожидание, что шаблон типа совпадёт с null. null is string s ложно. Обрабатывайте null отдельным case или через is null.
  • Чтение переменной шаблона там, где совпадение могло не произойти. После a is int n || ... переменная n не присвоена (CS0165).
  • Длинные switch по типам вместо виртуальных методов. Если каждый новый подкласс требует правки одного и того же switch в пяти местах, поведению место в иерархии классов. Шаблоны хороши для данных извне (формы JSON, сообщения, кортежи входных данных) и для закрытых наборов типов, которые вы контролируете.
  • Забытая ветвь для всех остальных в switch-выражении. Несовпавший вход выбрасывает исключение во время выполнения; читайте предупреждение CS8509.

Часто задаваемые вопросы

Что такое сопоставление с образцом в C#?

Сопоставление с образцом проверяет значение на соответствие форме и при совпадении может в том же шаге извлечь его части в переменные. if (shape is Circle c) проверяет тип и даёт вам типизированную c; order is { Status: "paid", Total: > 100 } проверяет свойства. Шаблоны используются в выражениях is, операторах switch и switch-выражениях.

Как использовать is с переменной в C#?

if (obj is Customer c) { ... } проверяет, что obj это Customer (не null), и присваивает его переменной c типа Customer внутри if. Это заменяет старую пару var c = obj as Customer; if (c != null). Переменная определённо присвоена только там, где проверка заведомо истинна, поэтому после if (!(obj is Customer c)) return; переменную c можно использовать.

Чем is null отличается от == null в C#?

x is null всегда проверяет саму ссылку. x == null вызывает оператор == типа, если тип его перегружает, что обычно даёт тот же ответ, но не гарантированно. Для противоположной проверки пишите x != null, !(x is null) или x is not null начиная с C# 9.

Как работает switch с шаблонами и when в C#?

Метка case может содержать шаблон плюс условие when: case Order o when o.Total > 100:. Case совпадает, только если совпал шаблон и условие истинно, а case проверяются сверху вниз, поэтому более конкретные ставьте первыми. Компилятор сообщает CS8120, когда case никогда не может быть достигнут, потому что предыдущий уже его покрывает.

В какой версии C# появились какие шаблоны?

C# 7.0: шаблоны типа, констант и var в is и case, плюс when. C# 8: switch-выражения, шаблоны свойств, кортежей и позиционные. C# 9: реляционные (> 5), логические (and, or, not) и голые шаблоны типа. C# 10: расширенные шаблоны свойств ({ Address.City: "Porto" }). C# 11: шаблоны списков ([1, .., var last]).

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ