Каков правильный способ передачи исключения? (С#)
Мне интересно, какой правильный путь - передать исключение из одного метода в другой.
Я работаю над проектом, который разделен на слои Presentation (web), Business и Logic, а ошибки (например, SqlExceptions) должны передаваться по цепочке, чтобы уведомить веб-слой, когда что-то пойдет не так.
Я видел 3 основных подхода:
try
{
//error code
}
catch (Exception ex)
{
throw ex;
}
(просто ретрол)
try
{
//error code
}
catch (Exception ex)
{
throw new MyCustomException();
}
(создайте собственное исключение, так что зависимость от поставщика данных не передается)
а затем просто
//error code
(ничего не делать вообще, позволяя ошибке выходить из строя)
Естественно, что в блоке catch также происходит некоторая запись.
Я предпочитаю номер 3, а мой коллега использует метод 1, но ни один из нас не может действительно мотивировать почему.
Каковы преимущества/недостатки использования каждого метода? Есть ли лучший способ, о котором я не знаю? Есть ли наилучший способ?
Ответы
Ответ 1
Если вы ничего не делаете, вы должны просто позволить ему двигаться вверх, где кто-то его обработает.
Вы всегда можете обрабатывать часть (например, ведение журнала) и повторно бросать ее. Вы можете повторно бросить, просто отправив throw;
, не указывая имя ex.
try
{
}
catch (Exception e)
{
throw;
}
Преимущество в том, что вы можете убедиться в том, что какой-то механизм должен уведомить вас о том, что у вас есть ошибка, в которой вы не подозреваете, что она есть.
Но, в некоторых случаях, скажем, сторонняя сторона, вы хотите, чтобы пользователь обрабатывал ее, и когда в этом случае вы должны позволить ей продолжать пузыриться.
Ответ 2
Я думаю, вы должны начать с немного другого вопроса
Как мне ожидать, что другие компоненты будут взаимодействовать с исключениями из моего модуля?
Если потребители вполне способны обрабатывать исключения, отбрасываемые нижними/слоями данных, тогда просто ничего не делают. Верхний уровень способен обрабатывать исключения, и вы должны делать только минимальное количество, необходимое для поддержания вашего состояния, а затем ретронирование.
Если потребители не могут обрабатывать исключения на низком уровне, но вместо этого требуют немного более высоких уровней исключения, затем создайте новый класс исключений, который они могут обрабатывать. Но не забудьте передать исходное исключение внутреннее исключение.
throw new MyCustomException(msg, ex);
Ответ 3
Правильный способ повторного выброса исключения в С# выглядит следующим образом:
try
{
....
}
catch (Exception e)
{
throw;
}
См. этот поток для специфики.
Ответ 4
Используйте только блоки try/catch вокруг исключений, которые вы ожидаете и можете обрабатывать. Если вы поймаете что-то, с чем вы не справитесь, он победит цель try/catch, которая предназначена для обработки ожидаемых ошибок.
Ловля большого исключения редко бывает хорошей идеей. Когда вы впервые поймаете OutOfMemoryException, вы действительно сможете грациозно справиться с этим? Большинство API документируют исключения, которые может использовать каждый метод, и те должны быть единственными обработанными и только если вы можете изящно его обработать.
Если вы хотите обработать ошибку дальше вверх по цепочке, пусть она пузырится сама по себе, не поймав и не перевернув ее. Единственное исключение для этого было бы для ведения журналов, но регистрация на каждом шаге будет делать чрезмерный объем работы. Лучше всего документировать, где исключение можно ожидать, что ваши общедоступные методы позволят пузыриться и позволить потребителю вашего API принять решение о том, как его обрабатывать.
Ответ 5
Никто еще не указал на самое первое, о чем вы должны думать: какие угрозы?
Когда базовый уровень выдает исключение, произошло что-то ужасное и неожиданное. Возможно, неожиданная страшная ситуация произошла, потому что этот уровень находится под атакой враждебного пользователя. Последнее, что вы хотите сделать в этом случае, служит для злоумышленника подробным списком всего, что пошло не так и почему. Когда что-то пойдет не так на уровне бизнес-логики, нужно тщательно зарегистрировать всю информацию об исключении и заменить исключение на общий "Мы сожалеем, что что-то пошло не так, администрация была предупреждена, попробуйте еще раз",
Одна из вещей, которые нужно отслеживать, - это вся информация, которую вы имеете о пользователе, и что они делали, когда произошло исключение. Таким образом, если вы обнаружите, что один и тот же пользователь всегда вызывает проблемы, вы можете оценить, могут ли они исследовать вас слабости или просто использовать необычный угол приложения, которое недостаточно проверено.
Сначала создайте дизайн безопасности, и только потом подумайте о диагностике и отладке.
Ответ 6
Я видел (и держал) различные сильные мнения об этом. Ответ заключается в том, что я не думаю, что в настоящее время на С# существует идеальный подход.
В какой-то момент я почувствовал, что (по-видимому, для Java) исключение является частью двоичного интерфейса метода, а также типа возвращаемого типа и типов параметров. Но в С# это просто не так. Это ясно из того факта, что нет системы спецификации бросков.
Другими словами, вы можете, если хотите, относиться к тому, что только ваши типы исключений должны лететь из ваших библиотечных методов, поэтому ваши клиенты не зависят от внутренних данных вашей библиотеки. Но немногие библиотеки потрудились сделать это.
Официальная консультация команды С# заключается в том, чтобы поймать каждый конкретный тип, который может быть брошен методом, если вы считаете, что можете справиться с ними. Не поймите ничего, с чем вы не справитесь. Это подразумевает отсутствие инкапсуляции внутренних исключений на границах библиотек.
Но, в свою очередь, это означает, что вам нужна совершенная документация о том, что может быть выбрано данным методом. Современные приложения полагаются на насыпи сторонних библиотек, быстро эволюционирующих. Он издевается над статической системой набора текста, если они все пытаются поймать конкретные типы исключений, которые могут быть неверными в будущих комбинациях версий библиотеки, без проверки времени компиляции.
Поэтому люди делают это:
try
{
}
catch (Exception x)
{
// log the message, the stack trace, whatever
}
Проблема заключается в том, что это захватывает все типы исключений, в том числе те, которые в корне указывают на серьезную проблему, например, исключение с нулевой ссылкой. Это означает, что программа находится в неизвестном состоянии. В тот момент, когда это обнаружено, оно должно быть закрыто, прежде чем оно наносит некоторый ущерб постоянным данным пользователя (запускает файлы с ошибками, записи в базе данных и т.д.).
Скрытая проблема здесь - try/finally. Это отличная языковая особенность - действительно, это важно - но если достаточно серьезное исключение взлетает стек, должно ли это действительно заставлять, наконец, блоки работать? Вы действительно хотите, чтобы доказательства были уничтожены, когда произошла ошибка? И если программа находится в неизвестном состоянии, все эти важные блоки могут быть уничтожены.
Так что вы действительно хотите (обновлено для С# 6!):
try
{
// attempt some operation
}
catch (Exception x) when (x.IsTolerable())
{
// log and skip this operation, keep running
}
В этом примере вы должны написать IsTolerable
как метод расширения на Exception
, который возвращает false
, если самое внутреннее исключение - NullReferenceException
, IndexOutOfRangeException
, InvalidCastException
или любые другие типы исключений, которые у вас есть решение должно указывать на ошибку низкого уровня, которая должна остановить выполнение и потребовать расследования. Это "невыносимые" ситуации.
Это можно назвать оптимистичной обработкой исключений: предполагается, что все исключения допустимы, за исключением набора известных черных списков. Альтернативой (поддерживаемой С# 5 и более ранним) является "пессимистический" подход, в котором только известные исключенные белые списки считаются допустимыми, а что-либо еще необработанным.
Несколько лет назад пессимистический подход был официальной рекомендуемой позицией. Но в наши дни CLR сам ловит все исключения в Task.Run
, чтобы он мог перемещать ошибки между потоками. Это приводит к тому, что, наконец, блоки будут выполняться. Таким образом, по умолчанию CRL очень оптимистичен.
Вы также можете записаться с помощью AppDomain.UnhandledException, сохранить как можно больше информации для поддержки (по крайней мере, трассировка стека), а затем вызовите Environment.FailFast, чтобы завершить процесс до того, как будут выполняться блоки finally
(которые могут уничтожить ценную информацию, необходимую для исследования ошибка или выбросить другие исключения, которые скрывают исходный).
Ответ 7
Я не уверен, что действительно есть принятые лучшие практики, но IMO
try // form 1: useful only for the logging, and only in debug builds.
{
//error code
}
catch (Exception ex)
{
throw;// ex;
}
Не имеет никакого реального смысла, кроме аспекта ведения журнала, поэтому я бы сделал это только в сборке отладки. Поймать ретронирование дорого, поэтому у вас должна быть причина для оплаты этой стоимости, кроме как того, что вам нравится смотреть на код.
try // form 2: not useful at all
{
//error code
}
catch (Exception ex)
{
throw new MyCustomException();
}
Это не имеет никакого смысла. Он отбрасывает реальное исключение и заменяет его тем, которое содержит меньше информации о реальной реальной проблеме. Я вижу, возможно, это сделать, если я хочу увеличить исключение с некоторой информацией о том, что происходит.
try // form 3: about as useful as form 1
{
//error code
}
catch (Exception ex)
{
throw new MyCustomException(ex, MyContextInformation);
}
Но я бы сказал, что почти во всех случаях, когда вы не обрабатываете исключение, лучшая форма заключается в том, чтобы просто разрешить обработчик более высокого уровня.
// form 4: the best form unless you need to log the exceptions.
// error code. no try - let it percolate up to a handler that does something productive.
Ответ 8
Обычно вы поймаете только те исключения, которые, как вы ожидаете, могут обрабатывать и позволять приложениям работать нормально. В случае, если вы хотите получить дополнительную регистрацию ошибок, вы поймаете исключение, выполните регистрацию и перезапустите его с помощью "throw"; поэтому трассировка стека не изменяется. Пользовательские исключения обычно создаются с целью сообщить о конкретных ошибках вашего приложения.