Уявіть, що ви будуєте систему обробки платежів. Кожен платіж може бути різного типу: кредитна картка, PayPal, криптовалюта. Кожен тип має різні властивості, і вам потрібно приймати рішення на основі цих властивостей.
Класичний підхід призводить до "спагетті-коду":
if (payment is CreditCardPayment)
{
var ccPayment = (CreditCardPayment)payment;
if (ccPayment.Amount > 1000 && ccPayment.Country == "UA")
{
// Спеціальна логіка для великих платежів в Україні
ApplyFraudCheck(ccPayment);
}
}
else if (payment is PayPalPayment)
{
var ppPayment = (PayPalPayment)payment;
if (ppPayment.IsVerified && ppPayment.Amount < 5000)
{
// Інша логіка
ProcessQuickly(ppPayment);
}
}
// ... ще 10 типів платежів
Pattern Matching перетворює цей код на виразну, типобезпечну конструкцію:
var action = payment switch
{
CreditCardPayment { Amount: > 1000, Country: "UA" } => ApplyFraudCheck,
PayPalPayment { IsVerified: true, Amount: < 5000 } => ProcessQuickly,
CryptoPayment { Currency: "BTC", Amount: > 0.1m } => ProcessWithDelay,
_ => ProcessStandard
};
action(payment);
Щоб зрозуміти всю потужність Pattern Matching, варто зазирнути в його теоретичне коріння — алгебраїчні типи даних (ADT). ADT — це спосіб структурування даних, що походить з теорії типів і функціонального програмування.
Існує два основних види ADT:
abstract класом або interface, де кожен спадкоємець — це один із можливих "варіантів". Enum також є простою формою типу-суми.- **Приклад**: `Shape` може бути `Circle` **АБО** `Rectangle` **АБО** `Triangle`.
class, struct, record або tuple.
Point складається з X І Y.Pattern Matching — це синтаксичний механізм, створений спеціально для зручної роботи з ADT. Він дозволяє одночасно:
is Circle).Таким чином, Pattern Matching не просто "синтаксичний цукор", а фундаментальний інструмент для роботи з даними, який забезпечує виразність, безпеку та повноту перевірок (exhaustiveness).
Pattern Matching у C# еволюціонував поступово:
| Версія C# | Нововведення | Приклад |
|---|---|---|
| C# 7.0 | Type patterns, is expression з змінною | if (obj is string s) |
| C# 8.0 | Switch expressions, property patterns, positional patterns | x switch { Point(0, 0) => "Origin" } |
| C# 9.0 | Relational patterns (>, <), logical patterns (and, or, not) | x is >= 0 and < 100 |
| C# 10 | Extended property patterns | obj is { Prop.SubProp: value } |
| C# 11 | List patterns 🔥 | array is [1, 2, ..] |
Перед вивченням Pattern Matching вам потрібно знати:
Pattern (Патерн) — це шаблон, який описує структуру даних. Компілятор перевіряє, чи відповідає (matches) вираз цьому шаблону, і якщо так — може вилучити (extract) значення з виразу.
Аналогія з реального світу: Уявіть, що ви сортуєте листи на пошті. Ви маєте набір "шаблонів":
- "Якщо конверт великий і має червону марку → відправити у відділення A"
- "Якщо конверт маленький і адреса починається з 'Київ' → відділення B"
Pattern Matching працює так само: ви описуєте "шаблони" даних, і компілятор автоматично визначає, який шаблон підходить.
Pattern Matching можна застосувати у:
is expression — перевірка з можливістю вилучення змінноїswitch expression — вибір значення на основі patterns (C# 8+)switch statement — класичний switch з patterns у caseif (obj is string { Length: > 0 } s)
{
Console.WriteLine($"Непорожній рядок: {s}");
}
var result = obj switch
{
string { Length: > 0 } s => $"Рядок: {s}",
int i when i > 0 => $"Позитивне число: {i}",
_ => "Щось інше"
};
switch (obj)
{
case string { Length: > 0 } s:
Console.WriteLine($"Рядок: {s}");
break;
case int i when i > 0:
Console.WriteLine($"Число: {i}");
break;
default:
Console.WriteLine("Інше");
break;
}
Одна з найпотужніших фіч — перевірка вичерпності (exhaustiveness checking). Компілятор попереджає, якщо ви не покрили всі можливі варіанти:
enum Status { Pending, Approved, Rejected }
### Теоретичне значення Exhaustiveness
Перевірка вичерпності — це не просто зручність, а реалізація одного з ключових принципів безпеки типів: **коректність програми, доведена компілятором**.
- **Усунення помилок часу виконання**: Головна перевага — це уникнення помилок, пов'язаних з необробленими випадками (наприклад, `ArgumentOutOfRangeException` або несподівана поведінка). Компілятор змушує вас замислитись про всі можливі стани.
- **Безпечний рефакторинг**: Якщо ви додасте новий член до `enum` (наприклад, `Status.OnHold`), компілятор негайно покаже всі `switch` вирази, які стали неповними. Це значно спрощує підтримку та еволюцію коду.
- **Зв'язок з ADT**: Для алгебраїчних типів даних, де тип-сума представляє *всі можливі* форми даних, перевірка вичерпності гарантує, що ви обробили кожен варіант. Це робить код математично доказовим.
`switch` вираз, на відміну від `switch` інструкції, був спеціально розроблений для посилення цих гарантій.
string GetMessage(Status status) => status switch
{
Status.Pending => "Очікує",
Status.Approved => "Затверджено"
// ⚠️ warning CS8509: The switch expression does not handle all possible values
// (it is not exhaustive). For example, the pattern 'Status.Rejected' is not covered.
};
Виправлення — додати discard pattern _:
string GetMessage(Status status) => status switch
{
Status.Pending => "Очікує",
Status.Approved => "Затверджено",
_ => "Невідомий статус"
};
Type Pattern перевіряє, чи є вираз певного типу, і вилучає його у типобезпечну змінну.
// is expression
if (obj is TypeName variable)
{
// використовуємо variable
}
// switch expression
var result = obj switch
{
TypeName variable => /* використовуємо variable */,
_ => /* default */
};
isC# — це мова зі статичною типізацією, де тип кожної змінної відомий на етапі компіляції. Однак, завдяки поліморфізму, змінна базового типу (object, Shape) може містити об'єкт похідного типу.
Type Pattern — це міст між світами статичної та динамічної типізації:
data is string відбувається під час виконання (runtime), оскільки компілятор не може знати наперед, який саме тип буде зберігатися у змінній типу object.text типу string. Усередині блоку if ця змінна є повністю статично типізованою, що дає вам усі переваги IntelliSense та безпеки типів.Таким чином, патерн типу дозволяє "зазирнути" в реальний тип об'єкта під час виконання і, у разі успіху, повернутися до безпечного світу статичної типізації.
object data = "Hello, Pattern Matching!";
if (data is string text)
{
Console.WriteLine($"Це рядок довжиною {text.Length}");
// ✅ змінна 'text' доступна тут
}
// ❌ змінна 'text' недоступна тут
Що відбувається під капотом?
Компілятор генерує:
is operatorstringtextЦе замінює старий підхід:
if (data is string)
{
string text = (string)data; // unsafe cast
Console.WriteLine($"Довжина: {text.Length}");
}
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public record Triangle(double Base, double Height) : Shape;
public static double CalculateArea(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,
_ => throw new ArgumentException("Невідома фігура")
};
// Використання
var circle = new Circle(5);
Console.WriteLine($"Площа кола: {CalculateArea(circle):F2}");
// Виведе: Площа кола: 78.54
as Castingif (obj is string s)
Console.WriteLine(s.Length);
else
Console.WriteLine("Не рядок");
var s = obj as string;
if (s != null)
Console.WriteLine(s.Length);
else
Console.WriteLine("Не рядок");
Чому Type Pattern кращий?
s доступна лише у if блоці (scope safety)obj is int i)Property Pattern дозволяє перевірити властивості об'єкта без явного приведення типу.
expression is Type { Property1: pattern1, Property2: pattern2 } variable
public record User(string Name, int Age, string Country);
User user = new("Олександр", 25, "Україна");
if (user is { Age: >= 18, Country: "Україна" })
{
Console.WriteLine("Повнолітній громадянин України");
}
Можна перевіряти вкладені властивості:
public record Address(string City, string Country);
public record Customer(string Name, Address Address);
public record Order(Customer Customer, decimal Total);
Order order = new(
new Customer("Марія", new Address("Київ", "Україна")),
1500m
);
var message = order switch
{
{ Customer.Address.Country: "Україна", Total: > 1000 }
=> "Безкоштовна доставка по Україні!",
{ Customer.Address.Country: "Україна" }
=> "Доставка: 50 грн",
_ => "Міжнародна доставка: 200 грн"
};
Console.WriteLine(message);
// Виведе: Безкоштовна доставка по Україні!
Customer.Address.Country замість Customer: { Address: { Country: ... } }. Це значно покращує читабельність!public abstract record Payment(decimal Amount);
public record CardPayment(decimal Amount, string CardNumber) : Payment(Amount);
public record CashPayment(decimal Amount, string Currency) : Payment(Amount);
public static string ProcessPayment(Payment payment) => payment switch
{
CardPayment { Amount: > 1000, CardNumber: var card }
=> $"Картка {card}: потрібна додаткова перевірка",
CardPayment { Amount: <= 1000 }
=> "Оброблено карткою",
CashPayment { Currency: "UAH", Amount: < 10000 }
=> "Готівка прийнята",
CashPayment { Currency: not "UAH" }
=> "Валюту потрібно обміняти",
_ => "Невідомий тип платежу"
};
public record Product(string Name, decimal Price, int Stock);
public static string ValidateProduct(Product product) => product switch
{
{ Name.Length: 0 }
=> "Назва не може бути порожньою",
{ Price: <= 0 }
=> "Ціна має бути більше 0",
{ Stock: < 0 }
=> "Кількість не може бути від'ємною",
{ Name.Length: > 100 }
=> "Назва занадто довга (макс. 100 символів)",
_ => "OK"
};
var product = new Product("", 100m, 10);
Console.WriteLine(ValidateProduct(product));
// Виведе: Назва не може бути порожньою
Tuple Pattern дозволяє перевіряти кортежі (tuples) і приймати рішення на основі множини значень одночасно.
public enum Choice { Rock, Paper, Scissors }
public static string DetermineWinner(Choice player1, Choice player2)
=> (player1, player2) switch
{
(Choice.Rock, Choice.Scissors) => "Гравець 1 виграв!",
(Choice.Scissors, Choice.Paper) => "Гравець 1 виграв!",
(Choice.Paper, Choice.Rock) => "Гравець 1 виграв!",
(Choice.Scissors, Choice.Rock) => "Гравець 2 виграв!",
(Choice.Paper, Choice.Scissors) => "Гравець 2 виграв!",
(Choice.Rock, Choice.Paper) => "Гравець 2 виграв!",
_ => "Нічия!"
};
Console.WriteLine(DetermineWinner(Choice.Rock, Choice.Scissors));
// Виведе: Гравець 1 виграв!
public record Customer(string Name, bool IsPremium);
public record Product(decimal Price, string Category);
public static decimal CalculateDiscount(Customer customer, Product product)
=> (customer, product) switch
{
({ IsPremium: true }, { Category: "Electronics", Price: > 1000 })
=> product.Price * 0.20m, // 20% для преміум на дорогу електроніку
({ IsPremium: true }, _)
=> product.Price * 0.10m, // 10% для преміум на все інше
(_, { Category: "Books" })
=> product.Price * 0.05m, // 5% на книги для всіх
_ => 0m // без знижки
};
var customer = new Customer("Іван", true);
var product = new Product(1500m, "Electronics");
var discount = CalculateDiscount(customer, product);
Console.WriteLine($"Знижка: {discount:C}");
// Виведе: Знижка: 300,00 ₴
public enum DoorState { Closed, Open, Locked }
public enum Action { Push, Pull, Lock, Unlock }
public static DoorState TransitionState(DoorState current, Action action, bool hasKey)
=> (current, action, hasKey) switch
{
(DoorState.Closed, Action.Push, _) => DoorState.Open,
(DoorState.Open, Action.Pull, _) => DoorState.Closed,
(DoorState.Closed, Action.Lock, true) => DoorState.Locked,
(DoorState.Locked, Action.Unlock, true) => DoorState.Closed,
// Недозволені переходи
(DoorState.Locked, _, false) => throw new InvalidOperationException("Потрібен ключ!"),
(var state, _, _) => state // залишаємось у поточному стані
};
// Використання
var state = DoorState.Closed;
state = TransitionState(state, Action.Lock, true);
Console.WriteLine(state); // Виведе: Locked
Deconstruct є синтаксичним способом представити об'єкт як кортеж значень. Коли ви пишете:var (x, y) = myPoint;
Deconstruct(out var x, out var y) і використовує його для "розпакування" об'єкта.Positional Pattern є прямим наслідком цього механізму. Вираз point switch { (0, 0) => ... } по суті є скороченням для:point у тимчасовий кортеж (int, int).Tuple Pattern до цього тимчасового кортежу.Deconstruct. Це робить код більш уніфікованим та декларативним.
Use Case: Tuple patterns ідеально підходять для state machines, game logic, та будь-яких сценаріїв, де рішення залежить від комбінації множини умов.Positional Pattern використовує deconstruction для перевірки значень у певному порядку.
Тип повинен мати метод Deconstruct або бути record (records автоматично генерують deconstruction).
public record Point(int X, int Y);
public static string ClassifyPoint(Point point) => point switch
{
(0, 0) => "Початок координат",
(0, _) => "На осі Y",
(_, 0) => "На осі X",
(var x, var y) when x == y => "На діагоналі (y = x)",
(var x, var y) when x == -y => "На діагоналі (y = -x)",
(> 0, > 0) => "Перший квадрант",
(< 0, > 0) => "Другий квадрант",
(< 0, < 0) => "Третій квадрант",
_ => "Четвертий квадрант"
};
var point = new Point(5, 5);
Console.WriteLine(ClassifyPoint(point));
// Виведе: На діагоналі (y = x)
(0, _) означає "X дорівнює 0, Y — будь-яке значення". Символ _ — це discard pattern, який ігнорує значення.public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
// Метод Deconstruct дозволяє використовувати positional patterns
public void Deconstruct(out string firstName, out string lastName, out int age)
{
firstName = FirstName;
lastName = LastName;
age = Age;
}
}
public static string GreetPerson(Person person) => person switch
{
("Тарас", "Шевченко", _) => "Великий Кобзар!",
(var first, var last, >= 18) => $"Вітаємо, {first} {last}!",
(var first, _, < 18) => $"Привіт, {first}!",
_ => "Привіт!"
};
var person = new Person { FirstName = "Олена", LastName = "Коваленко", Age = 25 };
Console.WriteLine(GreetPerson(person));
// Виведе: Вітаємо, Олена Коваленко!
public record Point(int X, int Y);
public record Line(Point Start, Point End);
public static string AnalyzeLine(Line line) => line switch
{
((0, 0), (0, 0)) => "Це точка, а не лінія",
((var x1, var y1), (var x2, var y2)) when x1 == x2 => "Вертикальна лінія",
((var x1, var y1), (var x2, var y2)) when y1 == y2 => "Горизонтальна лінія",
((0, 0), _) => "Лінія починається з початку координат",
(_, (0, 0)) => "Лінія закінчується у початку координат",
_ => "Звичайна лінія"
};
var line = new Line(new Point(0, 0), new Point(5, 0));
Console.WriteLine(AnalyzeLine(line));
// Виведе: Горизонтальна лінія
Relational Patterns (C# 9+) дозволяють використовувати оператори порівняння: <, >, <=, >=.
Logical Patterns комбінують patterns за допомогою and, or, not.
public static string GetGrade(int score) => score switch
{
>= 90 => "Відмінно",
>= 75 and < 90 => "Добре",
>= 60 and < 75 => "Задовільно",
>= 0 and < 60 => "Незадовільно",
_ => "Невалідний бал"
};
Console.WriteLine(GetGrade(85));
// Виведе: Добре
and, or, notpublic static bool IsValidPassword(string password) => password switch
{
null or { Length: < 8 } => false,
{ Length: >= 8 and <= 20 } => true,
_ => false
};
// Альтернативний синтаксис з `is`
public static bool IsWorkingHour(int hour)
=> hour is >= 9 and < 18;
// Використання `not`
public static bool IsWeekend(DayOfWeek day)
=> day is DayOfWeek.Saturday or DayOfWeek.Sunday;
public static bool IsWorkday(DayOfWeek day)
=> day is not (DayOfWeek.Saturday or DayOfWeek.Sunday);
Для складної логіки використовуйте дужки:
public record Employee(string Name, int Age, string Department, decimal Salary);
public static bool IsEligibleForBonus(Employee emp) => emp switch
{
{ Age: >= 30, Department: "IT" or "Sales", Salary: < 50000 } => true,
{ Department: "Management", Salary: >= 50000 and < 100000 } => true,
_ => false
};
// З дужками для ясності
public static string ClassifyEmployee(Employee emp) => emp switch
{
{ Age: < 25 } and { Salary: < 30000 }
=> "Junior",
({ Age: >= 25 and < 40 } and { Salary: >= 30000 and < 60000 })
or { Department: "IT", Age: < 30 }
=> "Middle",
{ Age: >= 40 } or { Salary: >= 60000 }
=> "Senior",
_ => "Uncategorized"
};
public enum AlertLevel { Normal, Warning, Critical }
public static AlertLevel CheckTemperature(double celsius) => celsius switch
{
< -40 or > 60 => AlertLevel.Critical,
>= -40 and < 0 => AlertLevel.Warning,
>= 0 and <= 35 => AlertLevel.Normal,
> 35 and <= 60 => AlertLevel.Warning,
_ => throw new ArgumentOutOfRangeException(nameof(celsius))
};
Console.WriteLine(CheckTemperature(38));
// Виведе: Warning
List Patterns дозволяють перевіряти послідовності (arrays, lists, spans) та їх елементи.
<LangVersion>11</LangVersion>
int[] numbers = { 1, 2, 3 };
// Точна відповідність
Console.WriteLine(numbers is [1, 2, 3]); // True
Console.WriteLine(numbers is [1, 2, 4]); // False
// Перевірка довжини
Console.WriteLine(numbers is [_, _, _]); // True (3 елементи)
Console.WriteLine(numbers is [_, _]); // False (не 2 елементи)
..)Slice pattern (..) відповідає "будь-якій кількості елементів":
int[] array = { 1, 2, 3, 4, 5 };
// Перший елемент — 1, решта — будь-що
Console.WriteLine(array is [1, ..]); // True
// Останній елемент — 5
Console.WriteLine(array is [.., 5]); // True
// Перший — 1, останній — 5, середина — будь-що
Console.WriteLine(array is [1, .., 5]); // True
// Хоча б 3 елементи
Console.WriteLine(array is [_, _, _, ..]); // True
varstring[] words = { "Hello", "Pattern", "Matching" };
if (words is [var first, .., var last])
{
Console.WriteLine($"Перше слово: {first}, Останнє: {last}");
// Виведе: Перше слово: Hello, Останнє: Matching
}
// Захоплення середини
if (words is [_, .. var middle, _])
{
Console.WriteLine($"Середина: {string.Join(", ", middle)}");
// Виведе: Середина: Pattern
}
int[] data = { 1, 5, 10, 15, 20 };
### Теоретичне підґрунтя: Head/Tail та рекурсивна обробка
List Patterns запозичують ідеї з функціональних мов (Lisp, Haskell, F#), де рекурсивна обробка списків через розбиття на "голову" (перший елемент) та "хвіст" (решта списку) є фундаментальною концепцією.
- `[var first, ..var rest]` — це C# еквівалент патерна `head::tail` в F#.
- `[]` — порожній список, який є базовим випадком для багатьох рекурсивних алгоритмів.
Хоча в C# рекурсивна обробка колекцій менш поширена, ніж цикли або LINQ, List Patterns відкривають нові можливості для декларативного програмування. Вони дозволяють виразно описувати структуру послідовності, що особливо корисно в таких задачах, як:
- Парсинг мов та протоколів.
- Аналіз послідовностей подій.
- Реалізація алгоритмів, що працюють з даними як з потоком.
Ця можливість робить код для роботи з колекціями значно чистішим і менш схильним до помилок, пов'язаних з індексами та межами масивів.
// Перший елемент 0 або 1, другий > 0, решта будь-що
Console.WriteLine(data is [0 or 1, > 0, ..]); // True
// Довжина 2 або 4, перший < 0, останній <= 0
int[] test1 = { -1, 0 };
int[] test2 = { -1, 0, 0, 1 };
static string Validate(int[] arr) => arr is [< 0, .. { Length: 2 or 4 }, > 0]
? "valid"
: "not valid";
Console.WriteLine(Validate(test1)); // not valid
Console.WriteLine(Validate(test2)); // not valid
public record Command(string Action, string[] Args);
public static string ParseCommand(string[] input) => input switch
{
[] => "Порожня команда",
["help"] => "Довідка: доступні команди...",
["create", var name] => $"Створення: {name}",
["delete", var name, "force"] => $"Видалення {name} з примусовим режимом",
["delete", var name] => $"Видалення: {name}",
["list", .. var options] => $"Список з опціями: {string.Join(", ", options)}",
["config", "set", var key, var value] => $"Налаштування {key} = {value}",
[var cmd, ..] => $"Невідома команда: {cmd}",
_ => "Невалідний ввід"
};
// Використання
string[] cmd1 = { "create", "project.txt" };
Console.WriteLine(ParseCommand(cmd1));
// Виведе: Створення: project.txt
string[] cmd2 = { "delete", "old.txt", "force" };
Console.WriteLine(ParseCommand(cmd2));
// Виведе: Видалення old.txt з примусовим режимом
string[] cmd3 = { "list", "--verbose", "--sort=name" };
Console.WriteLine(ParseCommand(cmd3));
// Виведе: Список з опціями: --verbose, --sort=name
Span<T> та ReadOnlySpan<T>, оскільки не створюють нових масивів для slice operations.Завжди обробляйте всі випадки або додайте _:
string GetStatus(int code) => code switch
{
200 => "OK",
404 => "Not Found",
500 => "Server Error",
_ => "Unknown" // ✅ обробляємо всі інші
};
string Classify(object obj) => obj switch
{
string { Length: > 100 } => "Довгий рядок", // ✅ специфічний
string => "Рядок", // ✅ загальний
int => "Число",
_ => "Щось інше"
};
string Classify(object obj) => obj switch
{
string => "Рядок", // ❌ загальний спочатку
string { Length: > 100 } => "Довгий рядок", // ⚠️ unreachable!
_ => "Щось інше"
};
var result = user switch
{
{ Age: >= 18, Country: "UA" } => "OK", // ✅ читабельно
_ => "NO"
};
var result = user switch
{
var u when u.Age >= 18 && u.Country == "UA" => "OK", // ❌ менш читабельно
_ => "NO"
};
// ❌ Занадто складно для читання
var result = data switch
{
{
User: {
Profile: {
Settings: {
Theme: "Dark",
Language: "UK" or "EN"
}
},
Age: >= 18 and < 65
},
IsActive: true
} => "Process",
_ => "Skip"
};
Краще — розбити на helper methods:
bool IsValidUser(Data data) => data.User is
{ Age: >= 18 and < 65, Profile.Settings: { Theme: "Dark", Language: "UK" or "EN" } };
var result = data switch
{
{ IsActive: true } when IsValidUser(data) => "Process", // ✅ читабельно
_ => "Skip"
};
// ❌ Дублювання логіки
var result = value switch
{
> 0 and < 10 => "Small",
>= 10 and < 100 => "Medium",
>= 100 => "Large",
< 0 => "Negative",
_ => "Zero"
};
// ✅ Логічна послідовність
var result = value switch
{
< 0 => "Negative",
0 => "Zero",
< 10 => "Small",
< 100 => "Medium",
_ => "Large"
};
Проблема: Switch expression не покриває всі можливі значення.
enum Color { Red, Green, Blue }
string GetName(Color c) => c switch
{
Color.Red => "Червоний",
Color.Green => "Зелений"
// ⚠️ CS8509: The switch expression does not handle all possible values
};
Рішення: Додати відсутній випадок або _:
string GetName(Color c) => c switch
{
Color.Red => "Червоний",
Color.Green => "Зелений",
Color.Blue => "Синій",
_ => throw new ArgumentOutOfRangeException(nameof(c))
};
Використання throw у _ branch для enum'ів — best practice, оскільки:
// ✅ Good
_ => throw new ArgumentOutOfRangeException(nameof(c), "Unexpected color")
// ❌ Bad (приховує проблему)
_ => "Unknown"
Проблема: Pattern ніколи не буде досягнутий через попередні patterns.
var result = obj switch
{
string => "String",
string { Length: > 10 } => "Long string", // ⚠️ unreachable
_ => "Other"
};
Рішення: Розташуйте patterns від більш специфічних до загальних:
var result = obj switch
{
string { Length: > 10 } => "Long string", // ✅ специфічний
string => "String", // ✅ загальний
_ => "Other"
};
Проблема: Один pattern "поглинає" інший.
var result = number switch
{
> 0 => "Positive",
> 10 => "Greater than 10", // ⚠️ unreachable (> 10 вже покрито > 0)
_ => "Not positive"
};
Рішення:
var result = number switch
{
> 10 => "Greater than 10", // ✅ спочатку більш специфічний
> 0 => "Positive",
_ => "Not positive"
};
Завдання: Створити функцію класифікації транспорту за допомогою Type та Property patterns.
public abstract record Vehicle(string Brand);
public record Car(string Brand, int Doors) : Vehicle(Brand);
public record Motorcycle(string Brand, int EngineCC) : Vehicle(Brand);
public record Bicycle(string Brand, bool IsElectric) : Vehicle(Brand);
// TODO: Реалізувати функцію ClassifyVehicle
// Повертає:
// - "Легковий автомобіль" для Car
// - "Потужний мотоцикл" для Motorcycle з EngineCC > 600
// - "Звичайний мотоцикл" для інших Motorcycle
// - "Електровелосипед" для Bicycle { IsElectric: true }
// - "Велосипед" для інших Bicycle
// - "Невідомий транспорт" для інших
public static string ClassifyVehicle(Vehicle vehicle)
{
// Ваш код тут
throw new NotImplementedException();
}
public static string ClassifyVehicle(Vehicle vehicle) => vehicle switch
{
Car => "Легковий автомобіль",
Motorcycle { EngineCC: > 600 } => "Потужний мотоцикл",
Motorcycle => "Звичайний мотоцикл",
Bicycle { IsElectric: true } => "Електровелосипед",
Bicycle => "Велосипед",
_ => "Невідомий транспорт"
};
// Тести
var car = new Car("Toyota", 4);
var bike = new Motorcycle("Harley", 750);
var ebike = new Bicycle("Trek", true);
Console.WriteLine(ClassifyVehicle(car)); // Легковий автомобіль
Console.WriteLine(ClassifyVehicle(bike)); // Потужний мотоцикл
Console.WriteLine(ClassifyVehicle(ebike)); // Електровелосипед
Завдання: Реалізувати калькулятор знижок з використанням Tuple та Relational patterns.
public record Customer(string Name, int LoyaltyYears, decimal TotalSpent);
public record Purchase(decimal Amount, string Category);
// TODO: Реалізувати CalculateDiscount
// Правила знижок:
// 1. LoyaltyYears >= 5 AND TotalSpent > 10000 AND Amount > 500: 25%
// 2. LoyaltyYears >= 3 AND Category == "Electronics": 15%
// 3. Amount >= 1000: 10%
// 4. Category == "Books": 5%
// 5. Інакше: 0%
public static decimal CalculateDiscount(Customer customer, Purchase purchase)
{
// Ваш код тут
throw new NotImplementedException();
}
public static decimal CalculateDiscount(Customer customer, Purchase purchase)
=> (customer, purchase) switch
{
({ LoyaltyYears: >= 5, TotalSpent: > 10000 }, { Amount: > 500 })
=> purchase.Amount * 0.25m,
({ LoyaltyYears: >= 3 }, { Category: "Electronics" })
=> purchase.Amount * 0.15m,
(_, { Amount: >= 1000 })
=> purchase.Amount * 0.10m,
(_, { Category: "Books" })
=> purchase.Amount * 0.05m,
_ => 0m
};
// Тести
var loyalCustomer = new Customer("Марія", 6, 15000);
var newCustomer = new Customer("Іван", 1, 500);
var expensivePurchase = new Purchase(600, "Electronics");
var bookPurchase = new Purchase(200, "Books");
Console.WriteLine(CalculateDiscount(loyalCustomer, expensivePurchase));
// Виведе: 150 (25%)
Console.WriteLine(CalculateDiscount(newCustomer, bookPurchase));
// Виведе: 10 (5%)
Завдання: Реалізувати парсер SQL-подібних команд з List Patterns.
public abstract record SqlCommand;
public record SelectCommand(string Table, string[] Columns, string? Where) : SqlCommand;
public record InsertCommand(string Table, Dictionary<string, string> Values) : SqlCommand;
public record UpdateCommand(string Table, Dictionary<string, string> Values, string Where) : SqlCommand;
public record DeleteCommand(string Table, string Where) : SqlCommand;
// TODO: Реалізувати ParseSqlCommand
// Приклади:
// ["SELECT", "*", "FROM", "users"] => SelectCommand("users", ["*"], null)
// ["SELECT", "name", "age", "FROM", "users", "WHERE", "id=1"]
// => SelectCommand("users", ["name", "age"], "id=1")
// ["INSERT", "INTO", "users", "name=John", "age=30"]
// => InsertCommand("users", {"name": "John", "age": "30"})
// ["DELETE", "FROM", "users", "WHERE", "id=1"]
// => DeleteCommand("users", "id=1")
public static SqlCommand ParseSqlCommand(string[] tokens)
{
// Ваш код тут
throw new NotImplementedException();
}
public static SqlCommand ParseSqlCommand(string[] tokens) => tokens switch
{
// SELECT * FROM table
["SELECT", "*", "FROM", var table]
=> new SelectCommand(table, ["*"], null),
// SELECT col1 col2 FROM table WHERE condition
["SELECT", .. var cols, "FROM", var table, "WHERE", var where]
=> new SelectCommand(table, cols.ToArray(), where),
// SELECT col1 col2 FROM table
["SELECT", .. var cols, "FROM", var table]
=> new SelectCommand(table, cols.ToArray(), null),
// INSERT INTO table col1=val1 col2=val2 ...
["INSERT", "INTO", var table, .. var pairs]
=> new InsertCommand(table, ParsePairs(pairs)),
// UPDATE table col1=val1 WHERE condition
["UPDATE", var table, .. var pairsAndWhere] when pairsAndWhere.Contains("WHERE")
=> ParseUpdate(table, pairsAndWhere),
// DELETE FROM table WHERE condition
["DELETE", "FROM", var table, "WHERE", var where]
=> new DeleteCommand(table, where),
_ => throw new ArgumentException("Невалідна SQL команда")
};
static Dictionary<string, string> ParsePairs(string[] pairs)
=> pairs.Select(p => p.Split('='))
.ToDictionary(parts => parts[0], parts => parts[1]);
static UpdateCommand ParseUpdate(string table, string[] tokens)
{
var whereIndex = Array.IndexOf(tokens, "WHERE");
var pairs = tokens[..whereIndex];
var where = tokens[whereIndex + 1];
return new UpdateCommand(table, ParsePairs(pairs), where);
}
// Тести
var cmd1 = ParseSqlCommand(["SELECT", "*", "FROM", "users"]);
Console.WriteLine(cmd1);
// SelectCommand { Table = users, Columns = [*], Where = }
var cmd2 = ParseSqlCommand(["SELECT", "name", "age", "FROM", "users", "WHERE", "id>10"]);
Console.WriteLine(cmd2);
// SelectCommand { Table = users, Columns = [name, age], Where = id>10 }
var cmd3 = ParseSqlCommand(["INSERT", "INTO", "products", "name=Phone", "price=500"]);
Console.WriteLine(cmd3);
// InsertCommand { Table = products, Values = {name: Phone, price: 500} }
obj is string s){ Age: >= 18 })(x, y) switch { ... })(0, 0) => "Origin")>= 0 and < 100, not null)[1, .., 5])| Pattern Type | Use Case | Приклад |
|---|---|---|
| Type | Робота з поліморфними типами | Обробка різних типів Shape |
| Property | Валідація об'єктів, фільтрація | Перевірка User.Age >= 18 |
| Tuple | State machines, множинні умови | Гра "Камінь-ножиці-папір" |
| Positional | Geометрія, структуровані дані | Класифікація Point(x, y) |
| Relational | Діапазони, рейтинги | Система оцінювання |
| List | Парсинг команд, алгоритми | Command-line parser |
if-else chains за допомогою pattern matching. Ви побачите, наскільки виразнішим та безпечнішим стане код!