Pattern matching и switch expressions

↑ 1.3 C♯: основы языка · ← Предыдущая · → Следующая

Обязательно~7 мин чтенияУровень: junior

Зачем это на собесе

pattern matching делает код компактным и безопасным: маппинг статусов, обработка разных типов ресурсов, валидация. Современный C#-код ждут на собесе, а не цепочки if/else с приведениями.

Объяснение

Виды паттернов

object value = GetValue();
 
// тип + переменная
if (value is Patient p) Console.WriteLine(p.LastName);
 
// константа и null
if (value is null) return;
if (status is AppointmentStatus.Cancelled) { ... }
 
// отрицание, и/или
if (value is not string) { ... }
if (age is >= 18 and < 65) { ... }            // relational + логический
if (code is "A" or "B" or "C") { ... }
 
// property pattern — проверка свойств (в т. ч. вложенных)
if (patient is { Address.City: "Минск", BirthDate.Year: < 2000 }) { ... }
 
// positional (для типов с Deconstruct, records)
if (point is (0, 0)) Console.WriteLine("origin");
 
// list patterns (C# 11)
if (args is [var first, .., var last]) { ... }
if (codes is ["ICD-10", _, ..]) { ... }

switch expression

string ToRu(AppointmentStatus s) => s switch
{
    AppointmentStatus.Booked    => "Записан",
    AppointmentStatus.Arrived   => "Пришёл",
    AppointmentStatus.Cancelled => "Отменён",
    _ => throw new ArgumentOutOfRangeException(nameof(s), s, null),
};
 
decimal Discount(Patient p) => p switch
{
    { Age: >= 65 }                    => 0.2m,
    { IsVeteran: true }               => 0.15m,
    { Age: < 18, HasInsurance: true } => 0.1m,
    null                              => throw new ArgumentNullException(nameof(p)),
    _                                 => 0m,
};
 
// обработка разных типов ресурсов (как в фабриках, мапперах)
string Describe(IResource r) => r switch
{
    Patient p            => $"Пациент {p.LastName}",
    Encounter { Status: "finished" } e => $"Визит {e.Id} завершён",
    Encounter e          => $"Визит {e.Id}",
    Observation o when o.Value > o.Threshold => "Отклонение от нормы",
    _                    => r.GetType().Name,
};

switch statement с паттернами

switch (ex)
{
    case PatientNotFoundException nf: return Results.NotFound(nf.Message);
    case ValidationException { Errors.Count: > 0 } ve: return Results.ValidationProblem(ve.ToDictionary());
    case OperationCanceledException: return Results.StatusCode(499);
    default: throw ex;
}

Компилятор проверяет полноту switch expression по enum и типам и предупреждает о недостижимых ветках (порядок важен: частное выше общего).

Нюансы и подводные камни

  • Switch expression без _ бросает SwitchExpressionException на непредусмотренном значении. Лучше явно _ => throw ... с понятным сообщением.
  • Большие switch по типам — возможный сигнал, что нужен полиморфизм (метод в каждом типе) или паттерн Strategy.
  • is с property pattern безопасен к null: x is { Name: "A" } вернёт false для null.
  • Паттерны не работают с произвольными выражениями справа: только константы, типы, relational-операторы.

Тестирование

[Theory]
[InlineData(70, false, 0.2)]
[InlineData(30, true, 0.15)]
[InlineData(30, false, 0)]
public void Discount_rules(int age, bool veteran, decimal expected) =>
    Discount(new Patient { Age = age, IsVeteran = veteran }).Should().Be(expected);

Вопросы с ответами

Связанные темы