Создавали ли вы пользовательские (кастомные) исключения в C#?

«Создавали ли вы пользовательские (кастомные) исключения в C#?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, создание собственных классов исключений — это лучшая практика для моделирования специфичных для домена ошибок приложения. Это позволяет четко разделять логику обработки сбоев.

Зачем создавать свои исключения?

  • Семантика: Исключение InvalidOrderStateException понятнее, чем общее InvalidOperationException.
  • Контекст: Можно добавить дополнительные свойства, несущие информацию об ошибке (например, OrderId, CurrentState).
  • Фильтрация: Обработчики могут ловить конкретные типы исключений, а не все подряд.

Как правильно создать пользовательское исключение:

  1. Наследование: Класс должен наследоваться от System.Exception или более конкретного исключения (например, ArgumentException).
  2. Конструкторы: Реализовать три стандартных конструктора для совместимости с системой сериализации.
  3. Соглашение об именах: Имя класса должно заканчиваться суффиксом Exception.
  4. Сериализация (для cross-domain): Пометить класс атрибутом [Serializable] и реализовать защищенный конструктор для десериализации.

Пример:

[Serializable] // Для поддержки сериализации через границы доменов приложения
public class InsufficientFundsException : InvalidOperationException
{
    // Дополнительные свойства для контекста ошибки
    public string AccountNumber { get; }
    public decimal CurrentBalance { get; }
    public decimal RequiredAmount { get; }

    // Стандартные конструкторы
    public InsufficientFundsException() { }
    public InsufficientFundsException(string message) : base(message) { }
    public InsufficientFundsException(string message, Exception innerException) 
        : base(message, innerException) { }

    // Конструктор с дополнительными данными
    public InsufficientFundsException(string accountNumber, decimal currentBalance, decimal requiredAmount)
        : base($"Account '{accountNumber}' has insufficient funds. Current: {currentBalance}, Required: {requiredAmount}")
    {
        AccountNumber = accountNumber;
        CurrentBalance = currentBalance;
        RequiredAmount = requiredAmount;
    }

    // Конструктор для десериализации (важен для cross-appdomain)
    protected InsufficientFundsException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        AccountNumber = info.GetString(nameof(AccountNumber));
        CurrentBalance = info.GetDecimal(nameof(CurrentBalance));
        RequiredAmount = info.GetDecimal(nameof(RequiredAmount));
    }

    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        base.GetObjectData(info, context);
        info.AddValue(nameof(AccountNumber), AccountNumber);
        info.AddValue(nameof(CurrentBalance), CurrentBalance);
        info.AddValue(nameof(RequiredAmount), RequiredAmount);
    }
}

// Использование
public void ProcessWithdrawal(string accountId, decimal amount)
{
    var account = GetAccount(accountId);
    if (account.Balance < amount)
    {
        throw new InsufficientFundsException(account.Id, account.Balance, amount);
    }
    // ... логика списания
}

Главное правило: Не создавайте новые исключения, если в BCL уже существует подходящее по смыслу (например, ArgumentNullException, InvalidOperationException). Свои исключения нужны для ошибок, уникальных для бизнес-логики вашего приложения.