Создавали ли вы пользовательские (кастомные) исключения в 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). Свои исключения нужны для ошибок, уникальных для бизнес-логики вашего приложения.

Ответ 18+ 🔞

А, ну это ж классика, блядь! Создавать свои исключения — это как завести личную банку с надписью «НЕ ОТКРЫВАТЬ, А ТО БУДЕТ ПИЗДЕЦ». Всегда полезно, когда хочешь не просто уронить всё к хуям, а объяснить, на каком именно этапе и почему начался пиздец.

А зачем вообще это надо, спросишь ты?

  • Читаемость: Когда в логах красуется OrderExplodedException — сразу ясно, что заказ нахуй полетел. А не какая-то абстрактная System.Exception, от которой нихуя не понятно.
  • Детализация: В своё исключение можно запихнуть любые данные — ID заказа, состояние, имя кота виноватого. Потом в обработчике всё это вытащишь и поймёшь, кто конкретно насрал.
  • Точечная поимка: Можно ловить именно PaymentFailedException, а не все исключения подряд, как слепой крот.

Как это сделать, не накосячив? Да элементарно:

  1. Наследуйся от правильного предка. Обычно от Exception или его детей — InvalidOperationException, ArgumentException. Не изобретай велосипед.
  2. Сделай три базовых конструктора. Это как три ноги у табуретки — без одной будет шатко. Все фреймворки этого ждут.
  3. Имя — с суффиксом Exception. DatabaseDeadException, UserIsIdiotException — сразу видно, что это не просто класс, а объявление войны.
  4. Сериализация. Если твоё исключение может путешествовать между доменами или процессами (в WCF, удалённых вызовах), пометь класс [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($"На счету '{accountNumber}' нихуя нет. Сейчас: {currentBalance}, Надо: {requiredAmount}. Иди работай.")
    {
        AccountNumber = accountNumber;
        CurrentBalance = currentBalance;
        RequiredAmount = requiredAmount;
    }

    // Конструктор для десериализации (чтоб после путешествия объект собрался обратно)
    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);
    }
    // ... дальше логика, если денег вдруг хватило
}

И главное, запомни: не плоди сущностей, как угорелый. Если есть стандартное исключение, которое точно описывает проблему (ArgumentNullException, FileNotFoundException), — используй его. Свои исключения — это для ошибок твоего домена, твоей бизнес-логики. Когда нужно крикнуть не просто «ошибка!», а «ошибка, потому что пользователь опять сделал хуйню!».