Обробка виключень у C#

pic

Як розробник з багаторічним досвідом роботи з JavaScript і Node.js, я спочатку вважав, що обробка помилок у C# — це просто інша синтаксична форма для тих самих концептів. Однак реальність виявилася значно складнішою — від великої ієрархії виключень до механізмів серіалізації та конвенцій, що вимагають більш суворого підходу.

Перші кроки з виключеннями в C

Один з перших фрагментів коду, який привернув мою увагу при вивченні C#, був таким:

[Serializable]  
public class CloudStorageException(  
 string message,  
 Exception? exception = null)  
 : MyCustomException($"Cloud storage error: {message}", exception)  
{ }

Для когось, хто має досвід роботи з JavaScript, де виключення — це просто екземпляр класу Error, наявність атрибута [Serializable] може здатися непотрібною. Однак у світі C# серіалізація виключень є критично важливою, особливо в розподілених додатках.

Для порівняння, у JavaScript ми б написали це значно простіше:

class CloudStorageException extends Error {  
 constructor(message, originalError = null) {  
 super(`Cloud storage error: ${message}`);  
 this.originalError = originalError;  
 }  
}

Що означає Serializable?

Serializable в C# використовується для включення серіалізації — процесу перетворення об'єкта в формат, який можна зберігати (наприклад, на диску) або передавати (наприклад, по мережі), а потім відновити в його початкову форму через десеріалізацію.

Серіалізація виключень в C# дозволяє зберігати стан виключення так, щоб його можна було передавати, зберігати або відновлювати в іншому контексті. Це особливо корисно в розподілених системах, де виключення можуть передаватися між різними процесами чи машинами.

Роль Serializable в серіалізації виключень

  • Атрибут [Serializable] необхідний для того, щоб виключення можна було серіалізувати. Це стосується як вбудованих .NET виключень, так і користувацьких виключень, визначених розробником.
  • Якщо клас виключення не позначений як Serializable, спроба його серіалізувати призведе до помилки під час виконання.

Коли використовувати Serializable для виключень?

Старі додатки .NET Framework вимагають використання атрибута [Serializable] для користувацьких класів виключень. Це особливо важливо для віддаленої комунікації, наприклад, коли старі додатки використовують Windows Communication Foundation (WCF). У таких випадках серіалізація виключень необхідна для належної роботи додатка.

Здається, що в сучасних додатках це вже не використовується, і замість цього надається перевага System.Text.Json. На жаль, я не знайшов чітке пояснення, яке я повністю розумію на цьому етапі. Якщо ви знаєте більше про це, будь ласка, поділіться в коментарях.

Ключові відмінності в підходах

Під час навчання я виявив кілька основних відмінностей між C# та JavaScript:

1. Ієрархія виключень

У C# всі виключення наслідують від класу System.Exception, що забезпечує чітку структуру та категоризацію. JavaScript набагато більш гнучкий в цьому — виключенням може бути практично будь-який об'єкт.

2. Обробка помилок і їх поширення

На перший погляд, обробка помилок у C# і JavaScript/TypeScript може здаватися подібною. Однак реальні відмінності стають очевидними у складніших сценаріях.
Давайте розглянемо приклад обробки помилок у робочому процесі обробки замовлень:

// c#  
public async Task ProcessOrderAsync(int orderId)  
{  
 try   
 {  
 var order = await _orderRepository.GetOrderAsync(orderId);  
 await _paymentService.ProcessPaymentAsync(order);  
 }  
 catch (OrderNotFoundException ex)  
 {  
 // Спеціфічний тип виключення дозволяє точно обробити помилку  
 _logger.LogWarning(ex, "Order {OrderId} not found", orderId);  
 throw; // Зберігаємо оригінальний стек викликів  
 }  
 catch (PaymentException ex) when (ex.ErrorCode == PaymentErrorCode.InsufficientFunds)  
 {  
 // Використання патерн-матчингу у catch - унікальна особливість C#  
 await _notificationService.NotifyCustomerAboutPaymentIssue(orderId);  
 throw new OrderProcessingException("Payment failed", ex);  
 }  
}
// TypeScript  
async function processOrder(orderId: number): Promise {  
 try {  
 const order = await orderRepository.getOrder(orderId);  
 await paymentService.processPayment(order);  
 } catch (error) {  
 // Необхідно перевірити тип помилки під час виконання  
 if (error instanceof OrderNotFoundError) {  
 logger.warn(`Order ${orderId} not found`);  
 throw error;  
 }  

 // Перевірка деталей помилки менш елегантна  
 if (error instanceof PaymentError && error.code === 'INSUFFICIENT_FUNDS') {  
 await notificationService.notifyCustomerAboutPaymentIssue(orderId);  
 throw new OrderProcessingError('Payment failed', error);  
 }  

 // Зазвичай маємо загальний блок для невідомих помилок  
 throw error;  
 }  
}

C# має кілька важливих переваг в обробці помилок:

  1. Патерн-матчинг у блоці catch за допомогою умови when — дозволяє елегантно та точно обробляти конкретні випадки.
  2. Глибока інтеграція з типами — немає потреби перевіряти типи виключень під час виконання.
  3. Прогнозована поведінка стека викликів — простий throw зберігає оригінальний стек викликів.
  4. Немає потреби в загальних блоках для обробки невідомих випадків.

Найкращі практики для ловлення виключень у C

До цього часу я вивчив кілька основних принципів обробки виключень у C#:

  1. Ловіть специфічні виключення — уникайте загального catch (Exception) на користь специфічних типів виключень.
  2. Використовуйте блок finally — особливо важливо при управлінні ресурсами.
  3. Створюйте спеціалізовані класи виключень — замість використання загального Exception, варто створювати спеціалізовані класи, як от CloudStorageException.

Висновок

У цій статті я поділився своїм досвідом переходу від обробки помилок у JavaScript до C#. Ключові відмінності, які я виявив, включають:

  • Більш суворий підхід до обробки помилок у C#, що вимагає точного оброблення конкретних типів виключень.
  • Важливість атрибута [Serializable] у контексті ловлення виключень.
  • Розширені механізми, такі як патерн-матчинг у блоках catch та прогнозована поведінка стека викликів.

Особливо цікавим для мене було те, як на перший погляд подібні механізми (try/catch) можуть мати значні відмінності у реалізації та найкращих практиках між цими мовами.

Це лише початок мого шляху в C#. У майбутніх статтях я поділюся ще більше відкриттів та висновків. Якщо вам цікава ця тема, підписуйтеся, щоб побачити більше постів на Medium.

Перекладено з: Exception Handling in C#

Leave a Reply

Your email address will not be published. Required fields are marked *