Уявіть, що ви створюєте систему обробки банківських транзакцій. Користувач вводить суму переказу, натискає "Підтвердити", і раптом... база даних недоступна. Або файл конфігурації пошкоджений. Або сума перевищує доступний баланс. Як ваша програма має відреагувати на ці ситуації?
Винятки (Exceptions) вирішують цю проблему елегантно. Вони дозволяють відокремити нормальну логіку програми від коду обробки помилок, автоматично передають інформацію про помилку вгору по стеку викликів і гарантують, що критичні ресурси будуть звільнені навіть у випадку збою.
Перед вивченням цієї теми вам потрібно розуміти:
IDisposable)Виняток (Exception) — це об'єкт, що представляє помилку або виняткову ситуацію, яка виникає під час виконання програми. Коли виникає виняток, нормальний потік виконання програми перериваєтьс я, і управління передається до найближчого обробника винятків (exception handler).
Теорія: Винятки vs Помилки Логіки
Важливо розуміти різницю між винятками та помилками логіки:
null посилання через неправильну логіку, вихід за межі масиву через некоректний індекс. Це має бути виправлено на етапі розробки, а не оброблено в runtime.Усі винятки в .NET успадковуються від базового класу System.Exception:
Ключові типи винятків:
| Тип | Коли виникає | Приклад |
|---|---|---|
ArgumentNullException | Параметр методу є null, а метод очікує значення | Передача null у string.Format |
ArgumentOutOfRangeException | Аргумент поза допустимим діапазоном | Від'ємний індекс масиву |
InvalidOperationException | Операція недійсна в поточному стані об'єкта | Читання з закритого Stream |
NullReferenceException | Спроба доступу до члена null посилання | string s = null; s.Length; |
IndexOutOfRangeException | Індекс масиву за межами | arr[100] для масиву з 10 елементів |
DivideByZeroException | Ділення на нуль (тільки для цілих чисел) | int x = 5 / 0; |
FileNotFoundException | Файл не знайдено | File.ReadAllText("missing.txt") |
IOException | Помилка введення-виведення | Немає прав доступу до файлу |
Теоретичні аспекти ієрархії винятків:
Ієрархія винятків у .NET побудована за принципом поліморфізму, що дозволяє обробляти винятки різних рівнів специфічності. Кожен тип винятку має свою роль:
SystemException — винятки, які генеруються безпосередньо CLR при виявленні помилок під час виконанняApplicationException — базовий клас для винятків, що створюються в додатках (хоча сучасна практика рекомендує успадковуватися безпосередньо від Exception)ArgumentException) успадковуються від більш загальних, що дозволяє ловити їх на різних рівнях ієрархіїЦя ієрархія дозволяє реалізувати точну обробку помилок — від конкретних до загальних, зберігаючи при цьому можливість обробки груп подібних винятків.
Кожен об'єкт винятку містить критично важливу діагностичну інформацію:
try
{
int[] numbers = [1, 2, 3];
Console.WriteLine(numbers[10]); // Вихід за межі масиву
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"Message: {ex.Message}");
Console.WriteLine($"StackTrace:\n{ex.StackTrace}");
Console.WriteLine($"Source: {ex.Source}");
Console.WriteLine($"TargetSite: {ex.TargetSite}");
}
/* Вивід:
Message: Index was outside the bounds of the array.
StackTrace:
at Program.<Main>$(String[] args) in Program.cs:line 4
Source: YourProgramName
TargetSite: Void <Main>$(System.String[])
*/
Ключові властивості:
Message: Зрозуміле людині повідомлення про помилку.StackTrace: Послідовність викликів методів, що призвела до помилки (критично для налагодження).InnerException: Вкладений виняток, якщо поточний виняток був викликаний іншим.Source: Ім'я додатку або об'єкта, що викликав виняток.TargetSite: Метод, що викинув виняток.Основна конструкція для обробки винятків складається з трьох блоків: try, catch та finally.
try
{
// Код, який може викинути виняток
int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
// Обробка конкретного типу винятку
Console.WriteLine($"Помилка: {ex.Message}");
}
finally
{
// Код, який виконається завжди (навіть якщо виняток не було)
Console.WriteLine("Очищення ресурсів...");
}
Як це працює:
try блок: Код всередині try виконується як зазвичай.catch блок.catch блок: Виконується, якщо тип винятку співпадає.finally блок: Виконується завжди — незалежно від того, виник виняток чи ні, навіть якщо в catch є return.Ви можете обробляти різні типи винятків по-різному:
try
{
string filePath = "data.json";
string content = File.ReadAllText(filePath);
int number = int.Parse(content);
int result = 100 / number;
}
catch (FileNotFoundException ex)
{
// Специфічна обробка для відсутнього файлу
Console.WriteLine($"Файл не знайдено: {ex.FileName}");
}
catch (FormatException ex)
{
// Обробка невалідного формату числа
Console.WriteLine($"Невалідний формат даних: {ex.Message}");
}
catch (DivideByZeroException ex)
{
// Обробка ділення на нуль
Console.WriteLine("Ділення на нуль!");
}
catch (Exception ex)
{
// Загальна обробка будь-якого іншого винятку
Console.WriteLine($"Невідома помилка: {ex.Message}");
}
catch перевіряються зверху вниз. Розміщуйте більш конкретні винятки перед більш загальними. Якщо ви розмістите catch (Exception ex) першим, він перехопить всі винятки, і наступні блоки ніколи не виконаються.Теорія: Коли Finally НЕ виконується
Блок finally виконується майже завжди, але є рідкісні винятки:
Environment.FailFast(): Негайне завершення процесу без очищення.В усіх нормальних сценаріях finally гарантовано виконається.
static string ReadFile(string path)
{
FileStream? stream = null;
try
{
stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
return reader.ReadToEnd(); // return не перешкоджає finally!
}
catch (IOException ex)
{
Console.WriteLine($"Помилка читання: {ex.Message}");
return string.Empty; // return з catch теж не перешкоджає
}
finally
{
// Виконається ЗАВЖДИ перед поверненням з методу
stream?.Dispose();
Console.WriteLine("Файл закрито");
}
}
finally для звільнення ресурсів (закриття файлів, з'єднань з БД, мережевих сокетів). Ще краще — використовуйте using statement, який автоматично генерує try-finally блок.Коли виникає виняток, CLR починає розмотування стеку викликів (stack unwinding) — пошук відповідного обробника:
Демонстрація Stack Unwinding:
using System;
using System.IO;
class Program
{
static void Main()
{
Console.WriteLine("Main: Початок");
try
{
ProcessData("data.txt");
Console.WriteLine("Main: Операція успішна");
}
catch (IOException ex)
{
Console.WriteLine($"Main: Перехопив IOException: {ex.Message}");
Console.WriteLine($"\nStack Trace:\n{ex.StackTrace}");
}
Console.WriteLine("Main: Завершення");
}
static void ProcessData(string filename)
{
Console.WriteLine(" ProcessData: Обробка даних з файлу...");
// Тут немає try-catch для FileNotFoundException
// Виняток пробросится до Main()
string content = ReadFile(filename);
Console.WriteLine($" ProcessData: Прочитано {content.Length} символів");
}
static string ReadFile(string path)
{
Console.WriteLine(" ReadFile: Відкриття файлу...");
// Якщо файл не існує — викинеться FileNotFoundException
// Стек розмотується: ReadFile -> ProcessData -> Main
string content = File.ReadAllText(path);
Console.WriteLine(" ReadFile: Файл прочитано");
return content;
}
}
/* Вивід (якщо файлу немає):
Main: Початок
ProcessData: Обробка даних з файлу...
ReadFile: Відкриття файлу...
Main: Перехопив IOException: Could not find file '/path/to/data.txt'.
Stack Trace:
at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access)
at System.IO.File.ReadAllText(String path)
at Program.ReadFile(String path) in Program.cs:line 38
at Program.ProcessData(String filename) in Program.cs:line 25
at Program.Main() in Program.cs:line 12
Main: Завершення
*/
Покрокове пояснення:
Main() викликає ProcessData() — додає frame у стек.ProcessData() викликає ReadFile() — ще один frame.ReadFile() викликає File.ReadAllText() — виникає FileNotFoundException.catch у ReadFile() — немає.ProcessData() — немає catch.Main() — знайдено catch (IOException)!FileNotFoundException є підкласом IOException, тому блок спрацьовує.catch.StackTrace видно весь шлях виклику — від File.ReadAllText() → ReadFile() → ProcessData() → Main(). Це дозволяє точно відстежити, де саме виникла помилка та як ми до неї дійшли.Що відбувається:
catch блок у поточному методі.caller).catch або досягнуто Main().C# 6.0 ввів фільтри винятків з ключовим словом when — потужний механізм для умовної обробки винятків.
try
{
await httpClient.GetStringAsync("https://api.example.com/data");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("Ресурс не знайдено (404)");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
Console.WriteLine("Не авторизовано (401). Оновіть токен");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Інша HTTP помилка: {ex.StatusCode}");
}
Теоретичні аспекти Exception Filters:
Exception filters з ключовим словом when представляють потужний механізм управляння потоком обробки винятків. На відміну від звичайної умови в catch блоці, фільтри обчислюються до того, як виняток вважається "спійманим". Це означає:
false, виняток не вважається обробленим у цьому catch блоціЦей механізм дозволяє зберігати оригінальний StackTrace навіть при використанні фільтрів, що робить налагодження значно простішим.
try
{
// ...
}
catch (HttpRequestException ex)
{
if (ex.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("404");
}
else if (ex.StatusCode == HttpStatusCode.Unauthorized)
{
Console.WriteLine("401");
}
else
{
Console.WriteLine("Інша помилка");
}
}
try
{
// ...
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("404");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
Console.WriteLine("401");
}
catch (HttpRequestException ex)
{
Console.WriteLine("Інша помилка");
}
Чому фільтри кращі:
| Критерій | If-Else в Catch | Exception Filters (when) |
|---|---|---|
| Читабельність | Вкладена логіка, важко читати | Плоска структура, легко сканувати |
| Debugging | Стек вже розмотано | Стек не розмотується до перевірки |
| Точки зупинки | Break on first chance не допомагає | Debugger зупиняється на правильному catch |
| Performance | Виняток завжди спіймано | Виняток пробрасується, якщо умова false |
Теорія: Стек не розмотується при перевірці фільтра
Це критична відмінність! Коли CLR зустрічає catch з when:
when.false — CLR продовжує пошук далі по стеку, не очищуючи поточний frame.true — тільки тоді виняток перехоплюється.Це означає, що стек залишається неушкодженим для налагодження, навіть якщо фільтр не спрацював.
Фільтри можуть викликати методи (але будьте обережні з побічними ефектами!):
static bool LogException(Exception ex)
{
Console.WriteLine($"[LOG] {DateTime.Now}: {ex.GetType().Name} - {ex.Message}");
return false; // Завжди повертаємо false, щоб НЕ перехоплювати
}
try
{
// Потенційно небезпечний код
RiskyOperation();
}
catch (Exception ex) when (LogException(ex))
{
// Цей блок НІКОЛИ не виконається, бо LogException повертає false
// Але виняток буде залоговано!
}
catch (SpecificException ex)
{
// Реальна обробка
}
when має бути чистою функцією (pure function) — не змінювати стан програми. Виключення: логування для діагностики. Але навіть тут будьте обережні — якщо логування викине виняток, він замінить оригінальний!throwЩоб викинути виняток, використовуйте ключове слово throw:
public void SetAge(int age)
{
if (age < 0 || age > 150)
{
throw new ArgumentOutOfRangeException(
nameof(age),
age,
"Вік має бути в діапазоні 0-150"
);
}
Age = age;
}
nameof() для передачі імені параметра. Це захистить від помилок при рефакторингу.Починаючи з C# 7.0, throw може використовуватися як вираз у таких контекстах:
// Null-coalescing з throw
public string Name { get; }
public User(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
// Тернарний оператор
public int Divide(int a, int b)
{
return b != 0
? a / b
: throw new DivideByZeroException();
}
// Expression-bodied member
public string GetFileName() =>
_fileName ?? throw new InvalidOperationException("Файл не встановлено");
// Lambda
Func<string, int> parser = s =>
int.TryParse(s, out int result)
? result
: throw new FormatException($"'{s}' не є числом");
throw vs throw exЦе критична різниця, яка впливає на налагодження:
try
{
DoSomething();
}
catch (Exception ex)
{
Console.WriteLine($"Помилка: {ex.Message}");
throw ex; // ❌ ВТРАТА StackTrace!
}
try
{
DoSomething();
}
catch (Exception ex)
{
Console.WriteLine($"Помилка: {ex.Message}");
throw; // ✅ Зберігає оригінальний StackTrace
}
Що відбувається:
throw ex: Створюється новий StackTrace з поточної точки. Ви втрачаєте інформацію про оригінальне місце помилки.throw (без аргументу): Пробрасує оригінальний виняток з незмінним StackTrace.throw ex без вагомої причини. Це ускладнює налагодження і приховує справжню причину помилки.Іноді потрібно "обгорнути" виняток нижчого рівня у більш зрозумілий контекстний:
public User LoadUser(int userId)
{
try
{
string json = File.ReadAllText($"users/{userId}.json");
return JsonSerializer.Deserialize<User>(json)!;
}
catch (FileNotFoundException ex)
{
// Обгортаємо технічний виняток у доменний
throw new UserNotFoundException(
userId,
$"Користувача {userId} не знайдено",
ex // ВАЖЛИВО: передаємо як InnerException!
);
}
catch (JsonException ex)
{
throw new DataCorruptedException(
$"Дані користувача {userId} пошкоджені",
ex
);
}
}
Переваги:
InnerException для налагодження.Створюйте власний виняток, якщо:
InsufficientFundsException зрозуміліший за InvalidOperationException.AccountId, RequestedAmount, AvailableBalance.ArgumentNullException чи InvalidOperationException достатньо описують ситуацію — використовуйте їх.// 1. Успадкуйте від Exception або більш конкретної підкласи
// 2. Назва має закінчуватися на "Exception"
// 3. Реалізуйте три конструктори (стандартна практика)
public class InsufficientFundsException : Exception
{
public string AccountId { get; }
public decimal RequestedAmount { get; }
public decimal AvailableBalance { get; }
// Конструктор без параметрів
public InsufficientFundsException()
{
}
// Конструктор з повідомленням
public InsufficientFundsException(string message)
: base(message)
{
}
// Конструктор з повідомленням та inner exception
public InsufficientFundsException(string message, Exception innerException)
: base(message, innerException)
{
}
// Додатковий конструктор з доменними даними
public InsufficientFundsException(
string accountId,
decimal requestedAmount,
decimal availableBalance)
: base($"Недостатньо коштів на рахунку {accountId}. " +
$"Запитано: {requestedAmount:C}, доступно: {availableBalance:C}")
{
AccountId = accountId;
RequestedAmount = requestedAmount;
AvailableBalance = availableBalance;
}
}
public class BankAccount
{
public string Id { get; init; }
public decimal Balance { get; private set; }
public void Withdraw(decimal amount)
{
if (amount > Balance)
{
throw new InsufficientFundsException(
Id,
amount,
Balance
);
}
Balance -= amount;
}
}
// Обробка
try
{
account.Withdraw(1000m);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($"Рахунок: {ex.AccountId}");
Console.WriteLine($"Бракує: {ex.RequestedAmount - ex.AvailableBalance:C}");
// Можна відобразити користувачу конкретну інформацію
ShowInsufficientFundsDialog(ex);
}
До .NET Core 3.0 винятки потрібно було робити серіалізованими для передачі між AppDomains:
[Serializable]
public class CustomException : Exception
{
public string CustomProperty { get; set; }
// Конструктор серіалізації
protected CustomException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
CustomProperty = info.GetString(nameof(CustomProperty))!;
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(CustomProperty), CustomProperty);
}
}
Принцип: Якщо виявлено некоректний стан, який може призвести до пошкодження даних — негайно викиньте виняток. Не намагайтеся "якось" продовжити роботу.
public void ProcessPayment(Payment payment)
{
if (payment == null)
{
// Мовчки ігноруємо — НЕ РОБІТЬ ТАК!
return;
}
// Продовжуємо з невалідними даними...
}
public void ProcessPayment(Payment payment)
{
ArgumentNullException.ThrowIfNull(payment); // C# 11+
if (payment.Amount <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(payment),
"Сума платежу має бути додатною"
);
}
// Тепер можемо бути впевнені, що дані валідні
_paymentGateway.Charge(payment);
}
Переваги:
ArgumentNullException.ThrowIfNull(), ArgumentException.ThrowIfNullOrEmpty() та подібні методи для лаконічної валідації.Один з найгірших анти-патернів:
❌ try
{
RiskyOperation();
}
catch (Exception)
{
// Порожній catch — "проковтнули" виняток
}
Чому це катастрофічно:
Що робити замість цього:
✅ try
{
RiskyOperation();
}
catch (Exception ex)
{
// ЗАВЖДИ хоч що-небудь робіть з винятком:
_logger.LogError(ex, "Помилка при виконанні операції");
// Можливі варіанти:
// 1. Пробросити далі
throw;
// 2. Обгорнути в інший виняток
throw new OperationFailedException("Операція не вдалася", ex);
// 3. Повернути Result<T> з помилкою (паттерн Result)
return Result.Failure<Data>(ex.Message);
}
try
{
var data = LoadFromDatabase();
ProcessData(data);
SaveToFile(data);
}
catch (Exception ex) // Ловимо ВСЕ — погана ідея
{
Console.WriteLine("Щось пішло не так");
}
try
{
var data = LoadFromDatabase();
ProcessData(data);
SaveToFile(data);
}
catch (SqlException ex) when (ex.Number == 1205) // Deadlock
{
// Повторити операцію
_logger.LogWarning("Deadlock, повторна спроба...");
Retry();
}
catch (IOException ex)
{
_logger.LogError(ex, "Помилка запису файлу");
throw new DataSaveException("Не вдалося зберегти дані", ex);
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "Невалідний стан даних");
throw;
}
// НЕ ловимо Exception — хай необроблені винятки падають
Чому це важливо:
ThreadAbortException, StackOverflowException, OutOfMemoryException — НЕ треба обробляти. Це критичні збої CLR.Exception, ви можете приховати помилки в бібліотеках, яких не очікували.using або try-finally для РесурсівРесурси (файли, з'єднання з БД, сокети) завжди треба звільняти:
FileStream stream = File.OpenRead("data.txt");
ProcessFile(stream);
stream.Dispose(); // НЕ виконається, якщо ProcessFile викине виняток!
FileStream? stream = null;
try
{
stream = File.OpenRead("data.txt");
ProcessFile(stream);
}
finally
{
stream?.Dispose();
}
using FileStream stream = File.OpenRead("data.txt");
ProcessFile(stream);
// Dispose() викличеться автоматично навіть при винятку
Анти-патерн: Exception-Driven Development
❌ // НІКОЛИ не робіть так!
try
{
int index = list.IndexOf(item);
return list[index];
}
catch (ArgumentOutOfRangeException)
{
return null; // Використовуємо виняток як if
}
Чому це погано:
Правильно:
✅ int index = list.IndexOf(item);
if (index >= 0)
{
return list[index];
}
return null;
Теорія: Performance Impact
Викидання та обробка винятку включає:
finally блоків.Приблизна вартість: ~100-1000x повільніше за звичайний if-else.
Коли це прийнятно:
Коли НЕ використовувати:
TryParse, TryGetValue.bool.| Best Practice | Anti-Pattern | Наслідки Anti-Pattern |
|---|---|---|
| Викидайте конкретні винятки | throw new Exception("error") | Важко обробляти, плутає викликаючий код |
| Ловіть конкретні винятки | catch (Exception) | Ховає критичні помилки, ускладнює налагодження |
throw; для re-throw | throw ex; | Втрата оригінального StackTrace |
using / try-finally для ресурсів | Виклик Dispose() без захисту | Витік ресурсів при винятку |
| Fail Fast при невалідних даних | Мовчазне ігнорування або default значення | Каскадні збої, важко налагоджувати |
| Логування ВСіХ винятків | Порожній catch { } | "Чорна діра" — помилка зникає безслідно |
| Винятки для виняткових ситуацій | Винятки для звичайного потоку | Погана продуктивність, плутана логіка |
ArgumentNullException.ThrowIfNull() | Перевірка вручну + throw | Більше коду, можливі помилки |
| Обгортання у доменні винятки з InnerException | Втрата оригінального винятку | Неможливо дізнатися справжню причину |
Result<T>, який містить або успішне значення, або інформацію про помилку. Це:FluentResults, ErrorOr, LanguageExt.❌ string name = user.Profile.Name; // user або Profile може бути null
Рішення:
✅ // Null-conditional operator
string? name = user?.Profile?.Name;
✅ // Pattern matching (C# 9+)
if (user is { Profile.Name: var name })
{
Console.WriteLine(name);
}
✅ // Nullable Reference Types (включити в .csproj)
<Nullable>enable</Nullable>
await — Винятки "Зникають"❌ async Task ProcessAsync()
{
DoWorkAsync(); // Забули await — виняток НЕ буде оброблено!
}
Рішення:
✅ async Task ProcessAsync()
{
await DoWorkAsync(); // Тепер виняток пробросится
}
✅ // Або якщо не потрібен результат, але треба обробити помилку
_ = DoWorkAsync().ContinueWith(task =>
{
if (task.IsFaulted)
{
_logger.LogError(task.Exception, "Помилка фонової задачі");
}
}, TaskScheduler.Default);
async void❌ async void Button_Click(object sender, EventArgs e)
{
await LoadDataAsync();
// Якщо тут виняток — програма впаде!
}
Рішення:
✅ async void Button_Click(object sender, EventArgs e)
{
try
{
await LoadDataAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Помилка завантаження");
MessageBox.Show($"Помилка: {ex.Message}");
}
}
Task або Task<T>.Debug → Windows → Exception SettingsSystem.NullReferenceException)catchtry
{
ProcessItems(items);
}
catch (Exception ex) when (ex.Message.Contains("critical"))
{
// Поставте breakpoint тут з умовою:
// ex.Message.Contains("specific error")
throw;
}
Напишіть метод SafeDivide(int a, int b), який:
a / bDivideByZeroException0 у випадку помилкиtry-catch з конкретним типом винятку. Не забудьте вивести повідомлення перед поверненням 0.public static int SafeDivide(int a, int b)
{
try
{
return a / b;
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Помилка: {ex.Message}. Повертаємо 0");
return 0;
}
}
// Використання
Console.WriteLine(SafeDivide(10, 2)); // 5
Console.WriteLine(SafeDivide(10, 0)); // Помилка: ... Повертаємо 0
Створіть клас EmailValidator з методом Validate(string email), який:
@InvalidEmailException з інформацією про emailInvalidEmailException має властивість InvalidEmail та відповідне повідомленняНапишіть тестовий код, який обробляє цей виняток.
InvalidEmailException : Exceptionpublic string InvalidEmail { get; }invalidEmailValidate використовуйте string.Contains("@")public class InvalidEmailException : Exception
{
public string InvalidEmail { get; }
public InvalidEmailException() { }
public InvalidEmailException(string message) : base(message) { }
public InvalidEmailException(string message, Exception innerException)
: base(message, innerException) { }
public InvalidEmailException(string invalidEmail, string message)
: base(message)
{
InvalidEmail = invalidEmail;
}
}
public class EmailValidator
{
public static void Validate(string email)
{
if (string.IsNullOrWhiteSpace(email) || !email.Contains("@"))
{
throw new InvalidEmailException(
email,
$"Email '{email}' не є валідним: відсутній символ '@'"
);
}
}
}
// Використання
try
{
EmailValidator.Validate("testexample.com");
}
catch (InvalidEmailException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($"Проблемний email: {ex.InvalidEmail}");
}
Реалізуйте метод ExecuteWithRetry<T>(Func<T> operation, int maxAttempts), який:
operation до maxAttempts разівHttpRequestException з кодом 429 (Too Many Requests) або 503 (Service Unavailable)for для спробcatch (HttpRequestException ex) when (ex.StatusCode == ...)Thread.Sleep(100 * (int)Math.Pow(2, attempt)) для експоненційного очікуванняthrow; після останньої спробиusing System.Net;
public class RetryHelper
{
public static T ExecuteWithRetry<T>(Func<T> operation, int maxAttempts)
{
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
return operation();
}
catch (HttpRequestException ex)
when (ex.StatusCode == HttpStatusCode.TooManyRequests ||
ex.StatusCode == HttpStatusCode.ServiceUnavailable)
{
if (attempt == maxAttempts)
{
Console.WriteLine($"❌ Вичерпано всі {maxAttempts} спроби");
throw; // Пробрасуємо після останньої спроби
}
int delayMs = 100 * (int)Math.Pow(2, attempt - 1);
Console.WriteLine(
$"⚠️ Спроба {attempt}/{maxAttempts} не вдалася " +
$"({ex.StatusCode}). Очікування {delayMs}ms..."
);
Thread.Sleep(delayMs);
}
// Інші винятки пробрасуються негайно
}
throw new InvalidOperationException("Неочікуваний стан");
}
}
// Використання (приклад з mock)
try
{
int attempt = 0;
var result = RetryHelper.ExecuteWithRetry(() =>
{
attempt++;
if (attempt < 3)
{
throw new HttpRequestException(
"Service Unavailable",
null,
HttpStatusCode.ServiceUnavailable
);
}
return "Success!";
}, maxAttempts: 5);
Console.WriteLine($"✅ Результат: {result}");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Операція не вдалася: {ex.Message}");
}
У цьому розділі ви вивчили:
- Винятки як механізм обробки помилок
- Ієрархія винятків та клас `System.Exception`
- Розмотування стеку (stack unwinding)
2. Try-Catch-Finally:
- Структура блоків обробки винятків
- Гарантії виконання `finally`
- Порядок множинних `catch` блоків
3. Exception Filters:
- Ключове слово `when` для умовної обробки
- Переваги над `if-else` у налагодженні
- Збереження стеку при перевірці фільтра
4. Throwing Exceptions:
- Оператор `throw` та throw expressions
- Критична різниця між `throw` та `throw ex`
- Обгортання винятків (exception wrapping)
5. Custom Exceptions:
- Коли створювати власні винятки
- Стандартний шаблон з трьома конструкторами
- Додавання доменних властивостей
6. Best Practices:
- ✅ Fail Fast — збій при першій помилці
- ✅ Ловіть конкретні винятки, не Exception
- ✅ Використовуйте using для ресурсів
- ❌ Не "ковтайте" винятки
- ❌ Не використовуйте винятки для контролю потоку
- ❌ Ніколи не пишіть throw ex
Task та async/await