Ответ
Пользовательское исключение — это класс, унаследованный от System.Exception или его производных, предназначенный для представления специфичных для предметной области ошибок.
Зачем создавать свои исключения?
- Семантика и ясность:
throw new OrderNotFoundException(...)понятнее, чемthrow new KeyNotFoundException(...). - Группировка обработки: Можно ловить конкретный тип исключения (
catch (PaymentFailedException ex)). - Дополнительные данные: Возможность добавить свойства с контекстом ошибки (ID заказа, код ошибки API).
Правила создания:
- Наследоваться от
Exception(илиApplicationException, но это менее распространено). - Добавлять стандартные конструкторы.
- Помечать класс атрибутом
[Serializable], если исключение может пересекать границы доменов приложения (например, в Remoting или WCF).
Пример правильно оформленного кастомного исключения:
[Serializable] // Для поддержки сериализации
public class InsufficientFundsException : Exception
{
// Дополнительные свойства для контекста
public string AccountId { get; }
public decimal CurrentBalance { get; }
public decimal RequiredAmount { get; }
// Стандартные конструкторы
public InsufficientFundsException() { }
public InsufficientFundsException(string message) : base(message) { }
public InsufficientFundsException(string message, Exception inner) : base(message, inner) { }
// Конструктор с дополнительными данными
public InsufficientFundsException(string accountId, decimal currentBalance, decimal requiredAmount)
: base($"Account '{accountId}' has insufficient funds. Current: {currentBalance}, Required: {requiredAmount}")
{
AccountId = accountId;
CurrentBalance = currentBalance;
RequiredAmount = requiredAmount;
}
// Конструктор для десериализации (важно для [Serializable])
protected InsufficientFundsException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
AccountId = info.GetString(nameof(AccountId));
CurrentBalance = info.GetDecimal(nameof(CurrentBalance));
RequiredAmount = info.GetDecimal(nameof(RequiredAmount));
}
}
// Использование в бизнес-логике
public void ProcessWithdrawal(string accountId, decimal amount)
{
var balance = GetBalance(accountId);
if (balance < amount)
{
// Бросаем информативное исключение с контекстом
throw new InsufficientFundsException(accountId, balance, amount);
}
// ... продолжение обработки
}
Рекомендация: Не создавайте новые исключения для ситуаций, которые уже точно описываются встроенными типами (ArgumentNullException, InvalidOperationException).
Ответ 18+ 🔞
А, ну вот, смотри, опять про эти ваши исключения кастомные. Ну ладно, раз уж спросил, объясню по-человечьи, а то в документации какую-то хуйню пишут, мозги выносят.
Представь, ты пишешь банковскую систему. И у тебя клиент пытается снять бабла больше, чем у него на счету. Можно, конечно, тупо выкинуть InvalidOperationException с текстом "Недостаточно средств". И что? А потом другой программист в другом конце кода будет ловить это исключение и думать: "Бля, а это что за невалидная операция? Может, я не тот метод вызвал? Или аргументы кривые?" Пиздец, короче, неочевидно.
Вот для этого и нужны свои исключения. Чтобы было понятно с полпинка, какая именно хрень случилась. throw new InsufficientFundsException(...) — это же сразу ясно, ёпта! Деньги кончились, вот и вся история. И ловить его можно точечно, не цепляя другие левые ошибки.
Как это правильно делать, чтобы не выглядеть как полный криворук:
- Наследуйся от
Exception. Это база, отец родной всех ошибок. ОтApplicationExceptionможно, но это уже такой старый дед, что его все забыли, лучше от обычногоException. - Стандартные конструкторы напиши, блядь. Их там четыре штуки обычно. Это как хороший тон, чтобы твое исключение могло вписаться в общую тусовку .NET.
- Атрибут
[Serializable]. Это если твоё исключение может путешествовать между разными процессами, доменами или вообще по сети. Если не уверен — ставь, хуже не будет. Но если поставил, не забудь про конструктор для десериализации, а то при распаковке получишь ещё одну ошибку поверх первой — вот это будет пиздец, да.
Главное — не выебывайся. Не создавай исключение UserPressedWrongButtonIdiotException. Для дурацких ситуаций уже есть нормальные встроенные типы. Используй ArgumentNullException, когда тебе передали null вместо объекта, или InvalidOperationException, когда состояние системы не позволяет сделать операцию. Свои — только для действительно уникальных, предметных ошибок твоего приложения.
Вот смотри, как это выглядит в коде, когда всё сделано по уму:
[Serializable] // На всякий пожарный, пусть будет сериализуемым
public class InsufficientFundsException : Exception
{
// Это наши фишки, которых в обычном Exception нет
public string AccountId { get; }
public decimal CurrentBalance { get; }
public decimal RequiredAmount { get; }
// Обязательные конструкторы, как у всех приличных исключений
public InsufficientFundsException() { }
public InsufficientFundsException(string message) : base(message) { }
public InsufficientFundsException(string message, Exception inner) : base(message, inner) { }
// А вот это — наш красавец, главный конструктор
public InsufficientFundsException(string accountId, decimal currentBalance, decimal requiredAmount)
: base($"На счету '{accountId}' нихуя не хватает. Сейчас: {currentBalance}, Надо: {requiredAmount}")
{
AccountId = accountId;
CurrentBalance = currentBalance;
RequiredAmount = requiredAmount;
}
// А это магия для сериализации. Если есть [Serializable], то и это нужно, иначе всё разъёбется при передаче.
protected InsufficientFundsException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
AccountId = info.GetString(nameof(AccountId));
CurrentBalance = info.GetDecimal(nameof(CurrentBalance));
RequiredAmount = info.GetDecimal(nameof(RequiredAmount));
}
}
// А вот как этим пользоваться в жизни
public void СнятьБаблища(string accountId, decimal amount)
{
var balance = УзнатьБаланс(accountId);
if (balance < amount)
{
// Всё, кидаем наше красивое, информативное исключение
throw new InsufficientFundsException(accountId, balance, amount);
}
// ... если денег хватает, то снимаем дальше
}
Видишь? Теперь любой, кто поймает InsufficientFundsException, сразу поймёт, в чём дело, и даже сможет достать из него AccountId, чтобы пользователю внятную ошибку показать. А не гадать, что за ебучий InvalidOperationException вылез.
Короче, смысл в том, чтобы код стал понятнее, а отладка — проще. Не плоди сущностей без необходимости, но и не тупи, используя Exception на все случаи жизни.