Ответ
Да, создание собственных классов исключений — это лучшая практика для моделирования специфичных для домена ошибок приложения. Это позволяет четко разделять логику обработки сбоев.
Зачем создавать свои исключения?
- Семантика: Исключение
InvalidOrderStateExceptionпонятнее, чем общееInvalidOperationException. - Контекст: Можно добавить дополнительные свойства, несущие информацию об ошибке (например,
OrderId,CurrentState). - Фильтрация: Обработчики могут ловить конкретные типы исключений, а не все подряд.
Как правильно создать пользовательское исключение:
- Наследование: Класс должен наследоваться от
System.Exceptionили более конкретного исключения (например,ArgumentException). - Конструкторы: Реализовать три стандартных конструктора для совместимости с системой сериализации.
- Соглашение об именах: Имя класса должно заканчиваться суффиксом
Exception. - Сериализация (для 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, а не все исключения подряд, как слепой крот.
Как это сделать, не накосячив? Да элементарно:
- Наследуйся от правильного предка. Обычно от
Exceptionили его детей —InvalidOperationException,ArgumentException. Не изобретай велосипед. - Сделай три базовых конструктора. Это как три ноги у табуретки — без одной будет шатко. Все фреймворки этого ждут.
- Имя — с суффиксом
Exception.DatabaseDeadException,UserIsIdiotException— сразу видно, что это не просто класс, а объявление войны. - Сериализация. Если твоё исключение может путешествовать между доменами или процессами (в 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), — используй его. Свои исключения — это для ошибок твоего домена, твоей бизнес-логики. Когда нужно крикнуть не просто «ошибка!», а «ошибка, потому что пользователь опять сделал хуйню!».