Підрозділ 9.1
Патерн типів
Пояснює type pattern у C#: перевірку типу через is, зіставлення з константами, null-перевірку, switch і додаткові умови через when.
9.1. Патерн типів
Що таке pattern matching
Pattern matching (зіставлення із зразком) — це механізм, який дозволяє перевірити значення на відповідність певному зразку (pattern) і, якщо перевірка успішна, виконати відповідну дію або отримати результат. Зразок може описувати тип, конкретне значення, структуру об'єкта, діапазон, логічну комбінацію умов тощо.
До появи pattern matching у C# 7 (і його розширень у 8, 9, 10, 11) програмісти виконували перевірки вручну через ланцюги if-else, оператори as і явні приведення типів. Ця система працювала, але була багатослівною, схильною до помилок і погано читалась. Pattern matching замінює її компактним, виразним і типобезпечним синтаксисом.
У розділі 9 ми розглянемо всі ключові типи зразків: типів, властивостей, кортежів, позиційних, реляційних, логічних і списків.
Патерн типів (type pattern)
Найпростіший і найбільш використовуваний різновид — патерн типів. Він перевіряє, чи є значення екземпляром певного типу, і якщо так — одразу прив'язує його до нової змінної потрібного типу:
значення is Тип зміннаТри дії в одному виразі: перевірка, приведення і прив'язка. Якщо перевірка не проходить — змінна не отримує значення (вона оголошена, але не присвоєна), і виконання йде в гілку else. Формально запис staff is Doctor doc у специфікації називається патерном оголошення (declaration pattern, C# 7); сам «патерн типу» без змінної — Doctor => … у switch expression — з'явився в C# 9. Нижче обидва називаємо патерном типів, як це прийнято на практиці.
Стара форма vs pattern matching
До C# 7 перевірка типу з подальшим cast виглядала так:
// Стара форма — два кроки, дублювання
if (staff is Doctor)
{
Doctor doc = (Doctor)staff; // cast треба повторити явно
doc.PrescribeMeds();
}
// Або через as + null-check
Doctor? doc = staff as Doctor;
if (doc != null)
doc.PrescribeMeds();З патерном типів:
// Нова форма — одна операція
if (staff is Doctor doc)
{
doc.PrescribeMeds(); // doc вже типу Doctor, не потрібен додатковий cast
}

Компілятор гарантує: якщо умова is Doctor doc виконалась, то doc не є null і гарантовано має тип Doctor. Ні зайвого cast-у, ні зайвої null-перевірки.
Клінічний приклад: ієрархія медичного персоналу
Визначимо клінічну ієрархію і застосуємо патерн типів:
Тут варто бути точним щодо області видимості. Змінна патерну, оголошена в умові if, належить не блоку if, а блоку, що його охоплює (правило «ширшої області видимості», C# 7). Тому doc і nurse видимі в усьому тілі ProcessStaff, і оголосити там іншу змінну doc не вдасться (CS0128). Захищає нас інше правило — певне присвоєння (definite assignment): компілятор дозволяє читати doc лише там, де патерн гарантовано спрацював. У гілці else чи після if без виходу звернення до doc дає помилку CS0165 «використання неприсвоєної змінної».
Саме ця комбінація робить можливим популярний прийом «ранній вихід»:
if (s is not Doctor doc)
return; // не лікар — виходимо
doc.PrescribeMeds(); // тут doc видима і гарантовано присвоєнаПеревірено на .NET 10: doc після такого if доступна, а звернення до неї в else звичайного if (s is Doctor doc) не компілюється (CS0165).
Патерн типів у switch
Патерни зручно застосовувати в конструкції switch. Тут є дві форми: switch statement приймає патерни в case з C# 7, а switch expression з'явився в C# 8:
switch statement (традиційний) — більш детальний, дозволяє break/return в кожному case:
switch (staff)
{
case Doctor doc when !doc.IsOnLeave:
doc.PrescribeMeds();
break;
case Nurse nurse:
nurse.ExecuteOrders();
break;
case null:
Console.WriteLine("Об'єкт null");
break;
default:
Console.WriteLine("Невідома роль");
break;
}switch expression (C# 8+) — компактний, повертає значення:
string role = staff switch
{
Doctor doc => $"Лікар: {doc.Specialty}",
Nurse nurse => $"Медсестра: {nurse.Ward}",
null => "null",
_ => "Інший персонал"
};Switch expression — це вираз, він повертає значення і може стояти в правій частині присвоєння. Switch statement — це оператор, він виконує дії. При pattern matching switch expression зазвичай компактніший і виразніший.
Додаткові умови: when
У switch statement до кожного case можна додати умову через when:
Constant pattern і null-перевірка
Патерн може порівнювати значення з константою, включаючи null:
// Перевірка конкретного рядка
if (diagnosis is "Гіпертонія II ст.")
Console.WriteLine("Стандартний протокол лікування");
// null-перевірка через патерн
if (patient is not null)
patient.PrintInfo();
// Або
if (patient is null)
throw new ArgumentNullException(nameof(patient));is null / is not null (друга форма — з C# 9) — це не просто синтаксичний цукор. Головна відмінність від == null / != null: патерн ніколи не викликає перевантажений оператор ==, а перевіряє саме посилання. Якщо в класі == перевантажено (навіть некоректно), x == null може повернути що завгодно, а x is null — завжди правду. Перевірено на .NET 10: для класу, чий == завжди повертає true, obj == null дає True, а obj is null — False. Крім того, такий запис виразно показує намір: «переконатись, що об'єкт існує».
Порядок case у switch: важливо
Switch обробляє варіанти зверху вниз і зупиняється на першому збігу. Якщо базовий тип іде першим — до похідних ніколи не дійде:
// НЕПРАВИЛЬНО — Doctor ніколи не буде спіймано:
switch (staff)
{
case MedicalStaff s: // спрацює першим для будь-якого типу!
// ...
case Doctor doc: // недосяжний код
// ...
}
// ПРАВИЛЬНО — конкретніші типи першими:
switch (staff)
{
case Doctor doc: // спочатку — конкретний
case Nurse nurse:
case MedicalStaff s: // потім — загальний
}Компілятор тут не просто попереджає — недосяжний case є помилкою компіляції: CS8120 у switch statement і CS8510 у switch expression («патерн уже оброблено попереднім arm-ом»). Тож неправильний порядок для типів з однієї ієрархії не пройде непоміченим. Але компілятор ловить лише очевидну недосяжність; якщо загальний case має умову when, він уже не вважається таким, що «з'їдає» все, і відповідальність за порядок повертається до програміста.
Антипатерн: ланцюг if-else з is та явним cast
До появи pattern matching стандартним способом перевірки типу була комбінація is для перевірки і явного cast (Тип)значення для доступу до членів. Цей підхід досі зустрічається у старому коді та є антипатерном у сучасному C#:
// АНТИПАТЕРН — стара форма: тип перевіряється двічі, потім cast вручну
void ProcessStaff(MedicalStaff s)
{
if (s is Doctor)
{
Doctor doc = (Doctor)s; // тип дублюється: is Doctor і (Doctor)
Console.WriteLine(doc.Specialty);
}
else if (s is Nurse)
{
Nurse nurse = (Nurse)s;
Console.WriteLine(nurse.Ward);
}
else if (s is Administrator)
{
Administrator admin = (Administrator)s;
Console.WriteLine(admin.Department);
}
}Цей стиль має три проблеми. По-перше, дублювання типу: перевірка is Doctor і наступний (Doctor)s — це дві операції над одним і тим самим фактом. По-друге, крихкість при рефакторингу: якщо хтось змінить перевірку is Doctor на is Surgeon, але забуде оновити cast — отримає InvalidCastException у runtime. По-третє, відсутність вичерпності: якщо до ієрархії додати новий тип Paramedic, компілятор не попередить, що він не оброблений у жодній гілці — виконання мовчки пройде мимо.
Стара форма через as + null-check трохи краща (немає подвійного cast), але все одно потребує двох кроків, а змінна doc має тип Doctor? і лишається доступною після if — компілятор дозволить звернутися до неї там, де вона може бути null (буде лише попередження nullable-аналізу). Змінна патерну теж видима після if, але правило певного присвоєння не дасть прочитати її там, де збігу не було:
// as + null-check: один cast, але binding поза блоком if
Doctor? doc = s as Doctor;
if (doc != null)
Console.WriteLine(doc.Specialty);
// doc досі видимий тут — хоч може бути nullПравильний підхід — switch expression із type pattern. Одна операція перевіряє тип і прив'язує змінну; вираз повертає значення; exhaustiveness перевіряється компілятором через _:
Switch expression перевіряє тип і прив'язує змінну в одній операції (Doctor doc) — жодного зайвого cast, жодного дублювання. Вбудований property pattern ({ IsOnLeave: true }) дозволяє уточнити умову без окремого if. Гілка _ — запасний варіант для нових типів: IDE одразу підказує додати case Paramedic, щойно такий клас з'явиться у ієрархії.
| Старий if-else + cast | Switch expression | |
|---|---|---|
| Кількість операцій для перевірки типу | 2 (is + явний cast) |
1 (type pattern) |
Ризик InvalidCastException |
Є при неузгодженому рефакторингу | Відсутній |
| Прив'язка змінної | Окрема оголошення після блоку if |
Вбудована (Doctor doc) |
| Перевірка властивостей | Окремий вкладений if |
Вбудований property pattern |
| Повертає значення | Ні — лише виконує дії | Так — вираз, можна присвоїти |
| Контроль нових типів | Мовчазний пропуск | IDE пропонує додати case |