Перехват исключений это половина обработки ошибок; вторая половина это выброс правильного исключения. Хорошо выбранное исключение точно сообщает вызывающему, что пошло не так и была ли это его ошибка или проблема состояния программы. Эта страница о выбросе; обработка описана на странице try catch.
Оператор throw и проверки аргументов
throw принимает объект исключения. Выполнение метода на этом останавливается, а исключение ищет обработчик выше по стеку вызовов. Самое частое применение это защитная проверка (guard clause): проверки в начале метода, которые отвергают некорректный ввод до начала работы.
Вывод:
ArgumentOutOfRangeException for parameter 'amount'
InvalidOperationException: The account is frozen.
Balance: 100
nameof(amount) даёт строку "amount" и остаётся правильным при переименовании параметра. Исключения аргументов сохраняют её в ParamName, по которому инструменты и логи указывают на плохой аргумент.
Защитные проверки упрощают остаток метода: после них код может считать ввод корректным. К тому же они срабатывают в месте ошибки, а не позволяют плохому значению путешествовать дальше и вызвать запутанный NullReferenceException тремя методами позже.
Какой тип исключения выбрасывать
Используйте встроенный тип, когда он описывает ситуацию; вызывающие уже знают, как его обрабатывать.
| Ситуация | Что выбрасывать |
|---|---|
Обязательный аргумент равен null | ArgumentNullException |
| Аргумент вне допустимого диапазона (отрицательное количество, индекс за концом) | ArgumentOutOfRangeException |
| Аргумент некорректен иначе (пустое имя, неверный формат идентификатора) | ArgumentException |
| Вызов недопустим в текущем состоянии объекта | InvalidOperationException |
Операция никогда не поддерживается этим типом (Add у коллекции только для чтения) | NotSupportedException |
| Метод ещё не написан | NotImplementedException |
Объект использован после Dispose | ObjectDisposedException |
| Операции с ограничением времени не хватило времени | TimeoutException |
Граница между первыми тремя строками и InvalidOperationException в том, кому нужно что-то изменить. Исключение аргумента говорит «вызовите иначе». InvalidOperationException говорит «вызов в порядке, но не сейчас».
Не выбрасывайте напрямую Exception, SystemException или ApplicationException: вызывающие не могут перехватить их, не перехватив заодно всё остальное. Не выбрасывайте сами и NullReferenceException, IndexOutOfRangeException или StackOverflowException; среда выполнения оставляет их для настоящих ошибок.
Выражения throw
До C# 7 throw был только оператором. Начиная с C# 7 он может быть и выражением в трёх местах, что превращает частые проверки в одну строку:
Вывод:
Ana <ana@example.com>
Null: name
ArgumentException: email
Обратите внимание, что ArgumentNullException наследуется от ArgumentException, поэтому catch (ArgumentException), поставленный первым, перехватил бы и случай с null. По той же причине важен порядок предложений catch, когда вы обрабатываете оба.
ThrowIfNull и родственные методы (.NET 6 и новее)
Современный .NET добавляет статические вспомогательные методы, которые пишут проверку и выброс за вас, автоматически подставляя имя параметра:
public void Ship(Order order, int quantity, string address)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // .NET 8
ArgumentException.ThrowIfNullOrWhiteSpace(address); // .NET 8
ObjectDisposedException.ThrowIf(disposed, this); // .NET 7
// ...
}
Они ведут себя так же, как написанные вручную if и throw, и держат каждую защитную проверку в одной строке. Для старых целевых платформ пишите форму с if, показанную выше.
Собственный класс исключения
Создавайте собственный тип исключения, когда вызывающим нужно перехватывать этот конкретный сбой отдельно или когда обработчику нужны данные, которые плохо передаются строкой сообщения.
Вывод:
Cannot withdraw 25 from a balance of 15.
Short by 10
Соглашения:
- Имя оканчивается на
Exception. - Класс наследуется от
Exception(или от более конкретного встроенного типа, когда это частный случай, напримерInvalidOperationException). - У него три стандартных конструктора: без аргументов, с сообщением и с сообщением плюс внутренним исключением. Собственные конструкторы добавляйте сверху.
- Дополнительные данные хранятся в свойствах только для чтения, задаваемых в конструкторе. Тогда обработчик может действовать по
e.Requested, а не разбирать сообщение.
Обёртывание с внутренним исключением
Когда сбой низкого уровня должен проявиться как сбой более высокого уровня, оберните его. Исходное исключение сохраняется как InnerException, поэтому никакая информация не теряется:
Вывод:
Setting 'port' must be a number, got '80a'.
Caused by: FormatException
Теперь вызывающий код оперирует понятиями конфигурации, которые он понимает, а запись в лог e.ToString() печатает всю цепочку, включая FormatException и его трассировку стека. Оборачивайте только тогда, когда добавляете смысл; обёртывание каждого исключения в общий MyAppException лишь заставляет обработчики копаться в InnerException.
Выбросить исключение или вернуть результат
Исключения предназначены для сбоев, которых вызывающий не ожидает при нормальной работе. Для рутинных исходов, например поиска, который часто ничего не находит, или пользовательского ввода, который часто некорректен, в .NET принят шаблон Try: вернуть bool и передать значение через параметр out.
public bool TryWithdraw(decimal amount, out string error)
{
if (amount > Balance) { error = "Insufficient funds."; return false; }
Balance -= amount;
error = null;
return true;
}
Многие типы предлагают оба варианта: int.Parse выбрасывает исключение, int.TryParse возвращает false; dict[key] выбрасывает исключение, dict.TryGetValue возвращает false. Выброс исключения стоит гораздо дороже возврата значения, поэтому ему не место на пути, который выполняется тысячи раз в секунду. Параметры out описаны на странице ref и out.
Хорошие сообщения
Сообщение исключения читает разработчик, смотрящий в лог. Пусть оно говорит, что было не так, и, если это безопасно, какое значение было ошибочным: «Quantity must be between 1 and 99, got 0.» лучше, чем «Invalid input.». Пишите полными предложениями и не помещайте в сообщения секреты вроде паролей и токенов, потому что они попадают в файлы логов.
Частые ошибки
- Выброс самого
Exception. Вызывающие не могут перехватить его избирательно; используйте конкретный тип. - Сообщение на месте имени параметра.
new ArgumentNullException("name")принимает имя параметра; сообщение идёт вторым. - Имена параметров, записанные вручную. Используйте
nameof(param), чтобы переименования их не ломали. - Собственные исключения без дополнительного смысла. Если подходит встроенный тип, используйте его.
- Потеря исходной ошибки при обёртывании. Всегда передавайте её как внутреннее исключение.
Часто задаваемые вопросы
Как выбросить исключение в C#?
Создайте объект исключения и выбросьте его: throw new ArgumentException("Amount must be positive", nameof(amount));. Выполнение останавливается на этой строке, и исключение поднимается по стеку вызовов к ближайшему подходящему catch. Выбирайте самый конкретный встроенный тип, описывающий проблему, или собственный тип, когда вызывающим нужно обрабатывать этот случай отдельно.
Как создать собственное исключение в C#?
Унаследуйте класс от Exception с именем, оканчивающимся на Exception, и дайте ему стандартные конструкторы: без аргументов, с сообщением и с сообщением и внутренним исключением, каждый из которых вызывает соответствующий конструктор base(...). Добавьте свойства только для чтения для данных, которые нужны обработчику, например идентификатора заказа или баланса.
Когда выбрасывать ArgumentException, а когда InvalidOperationException?
Выбрасывайте ArgumentException (или ArgumentNullException / ArgumentOutOfRangeException), когда вызывающий передал плохое значение: исправление в том, чтобы вызвать метод по-другому. Выбрасывайте InvalidOperationException, когда аргументы в порядке, но объект в неподходящем для вызова состоянии, например чтение из закрытого подключения или списание с замороженного счёта.
Что такое выражение throw в C#?
Начиная с C# 7 throw можно использовать как выражение в трёх местах: после ??, в любой ветви ?: и как тело члена или лямбды с телом-выражением. Например, _name = name ?? throw new ArgumentNullException(nameof(name)); присваивает или выбрасывает исключение в одной строке.
Что делает ArgumentNullException.ThrowIfNull?
Это статический вспомогательный метод из .NET 6: ArgumentNullException.ThrowIfNull(customer); выбрасывает ArgumentNullException с автоматически подставленным именем параметра, когда customer равен null, и ничего не делает в противном случае. Более поздние версии добавили похожие методы, например ArgumentException.ThrowIfNullOrEmpty (.NET 7) и ArgumentOutOfRangeException.ThrowIfNegative (.NET 8).