Зачем ловить и отбрасывать исключение в C#?

Я смотрю на статью C# - Объект передачи данных о сериализуемых DTO.

Статья включает в себя этот кусок кода:

public static string SerializeDTO(DTO dto) {
    try {
        XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
        StringWriter sWriter = new StringWriter();
        xmlSer.Serialize(sWriter, dto);
        return sWriter.ToString();
    }
    catch(Exception ex) {
        throw ex;
    }
}

Остальная часть статьи выглядит вменяемой и разумной (нубу), но этот try-catch-throw генерирует исключение WtfException... Разве это не эквивалентно тому, чтобы вообще не обрабатывать исключения?

Ergo:

public static string SerializeDTO(DTO dto) {
    XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
    StringWriter sWriter = new StringWriter();
    xmlSer.Serialize(sWriter, dto);
    return sWriter.ToString();
}

Или я упускаю что-то фундаментальное в обработке ошибок в C#? Это почти так же, как Java (за исключением проверенных исключений), не так ли? ... То есть они оба усовершенствовали C++.

Вопрос переполнения стека Разница между перебрасыванием улова без параметров и бездействием? кажется, поддерживает мое утверждение, что try-catch-throw является неоперативным.


РЕДАКТИРОВАТЬ:

Просто подвести итог для тех, кто найдет эту тему в будущем...

НЕ ДЕЛАЙТЕ

try {
    // Do stuff that might throw an exception
}
catch (Exception e) {
    throw e; // This destroys the strack trace information!
}

Информация трассировки стека может иметь решающее значение для определения первопричины проблемы!

ДЕЛАТЬ

try {
    // Do stuff that might throw an exception
}
catch (SqlException e) {
    // Log it
    if (e.ErrorCode != NO_ROW_ERROR) { // filter out NoDataFound.
        // Do special cleanup, like maybe closing the "dirty" database connection.
        throw; // This preserves the stack trace
    }
}
catch (IOException e) {
    // Log it
    throw;
}
catch (Exception e) {
    // Log it
    throw new DAOException("Excrement occurred", e); // wrapped & chained exceptions (just like java).
}
finally {
    // Normal clean goes here (like closing open files).
}

Поймать более конкретные исключения, прежде чем менее конкретные (так же, как Java).


Рекомендации:

18 ответов

Решение

Первый; способ, которым код в статье делает это, является злом. throw ex сбросит стек вызовов в исключении до точки, где находится этот оператор throw; потеря информации о том, где на самом деле было создано исключение.

Во-вторых, если вы просто ловите и перебрасываете так, я не вижу никакой дополнительной ценности, приведенный выше пример кода будет таким же хорошим (или, учитывая throw ex немного, даже лучше) без пробной ловли.

Однако есть случаи, когда вы можете захотеть перехватить и выбросить исключение. Регистрация может быть одним из них:

try 
{
    // code that may throw exceptions    
}
catch(Exception ex) 
{
    // add error logging here
    throw;
}

Не делай этого,

try 
{
...
}
catch(Exception ex)
{
   throw ex;
}

Вы потеряете информацию трассировки стека...

Либо делай,

try { ... }
catch { throw; }

ИЛИ ЖЕ

try { ... }
catch (Exception ex)
{
    throw new Exception("My Custom Error Message", ex);
}

Одна из причин, по которой вы можете захотеть сбросить, - это если вы обрабатываете разные исключения, например,

try
{
   ...
}
catch(SQLException sex)
{
   //Do Custom Logging 
   //Don't throw exception - swallow it here
}
catch(OtherException oex)
{
   //Do something else
   throw new WrappedException("Other Exception occured");
}
catch
{
   System.Diagnostics.Debug.WriteLine("Eeep! an error, not to worry, will be handled higher up the call stack");
   throw; //Chuck everything else back up the stack
}

C# (до C# 6) не поддерживает CIL "отфильтрованные исключения", что делает VB, поэтому в C# 1-5 одной из причин для повторного выброса исключения является то, что у вас недостаточно информации во время catch() чтобы определить, действительно ли вы хотите поймать исключение.

Например, в VB вы можете сделать

Try
 ..
Catch Ex As MyException When Ex.ErrorCode = 123
 .. 
End Try

... который не будет обрабатывать MyExceptions с различными значениями ErrorCode. В C# до v6 вам пришлось бы перехватывать и перебрасывать MyException, если ErrorCode не был 123:

try 
{
   ...
}
catch(MyException ex)
{
    if (ex.ErrorCode != 123) throw;
    ...
}

Начиная с C# 6.0 вы можете фильтровать, как с VB:

try 
{
  // Do stuff
} 
catch (Exception e) when (e.ErrorCode == 123456) // filter
{
  // Handle, other exceptions will be left alone and bubble up
}

Моя главная причина для того, чтобы иметь такой код:

try
{
    //Some code
}
catch (Exception e)
{
    throw;
}

так что я могу иметь точку останова в перехвате, который имеет экземпляр объекта исключения. Я делаю это много во время разработки / отладки. Конечно, компилятор предупреждает меня обо всех неиспользованных е, и в идеале они должны быть удалены перед сборкой релиза.

Они хороши во время отладки.

Действительная причина для повторного выброса исключений может заключаться в том, что вы хотите добавить информацию об исключении или, возможно, обернуть исходное исключение одним из ваших собственных решений:

public static string SerializeDTO(DTO dto) {
  try {
      XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
      StringWriter sWriter = new StringWriter();
      xmlSer.Serialize(sWriter, dto);
      return sWriter.ToString();
  }
  catch(Exception ex) {
    string message = 
      String.Format("Something went wrong serializing DTO {0}", DTO);
    throw new MyLibraryException(message, ex);
  }
}

Вы не хотите бросать ex - так как это потеряет стек вызовов. См. Обработка исключений (MSDN).

И да, команда try... catch не делает ничего полезного (кроме потери стека вызовов - так что на самом деле это еще хуже - если только по какой-то причине вы не захотели раскрыть эту информацию).

Разве это не эквивалентно тому, чтобы вообще не обрабатывать исключения?

Не совсем, это не то же самое. Сбрасывает трассировку стека исключения. Хотя я согласен, что это, вероятно, ошибка, и, следовательно, пример плохого кода.

Это может быть полезно, когда ваши функции программирования для библиотеки или DLL.

Эту структуру повторного выброса можно использовать для целенаправленного сброса стека вызовов, чтобы вместо просмотра исключения, генерируемого отдельной функцией внутри функции, вы получали исключение из самой функции.

Я думаю, что это просто используется для того, чтобы сгенерированные исключения были чище и не уходили в "корни" библиотеки.

Вопрос, который люди не упомянули, заключается в том, что, хотя языки.NET на самом деле не проводят должного различия, вопрос о том, следует ли предпринимать какие-либо действия при возникновении исключения и разрешит ли он его, на самом деле является отдельным вопросом. Есть много случаев, когда нужно предпринимать действия, основанные на исключениях, которые не имеют надежды разрешить, и есть случаи, когда все, что необходимо для "разрешения" исключения, - это размотать стек до определенной точки - никаких дальнейших действий не требуется.,

Из-за общей мудрости, что нужно "ловить" только то, с чем можно "справиться", большой код, который должен действовать при возникновении исключений, этого не делает. Например, большая часть кода получает блокировку, "временно" помещает охраняемый объект в состояние, которое нарушает его инварианты, затем переводит его в легитимное состояние и затем снимает блокировку, прежде чем кто-либо еще сможет увидеть объект. Если возникает исключение, когда объект находится в опасно недопустимом состоянии, обычной практикой является снятие блокировки с объектом, все еще находящимся в этом состоянии. Гораздо лучше было бы иметь исключение, которое возникает, когда объект находится в "опасном" состоянии, и недействительно блокирует блокировку, поэтому любая будущая попытка получить ее немедленно потерпит неудачу. Последовательное использование такого шаблона значительно улучшило бы безопасность обработки так называемых "покемонов", которая, по мнению IMHO, имеет плохую репутацию, в первую очередь из-за кода, который позволяет исключениям возникать, не предпринимая соответствующих действий.

В большинстве языков.NET единственным способом, позволяющим коду действовать на основе исключения, является catch (хотя он знает, что не собирается разрешать исключение), выполните соответствующее действие, а затем повторноthrow). Другой возможный подход, если коду не важно, какое исключение выдается, это использовать ok флаг с try/finally блок; установить ok флаг для false до блока и true до выхода из блока и до любого return это в пределах блока. Тогда внутри finally, предположим, что если ok не установлено, должно быть исключение. Такой подход семантически лучше, чем catch/throw, но уродлив и менее ремонтопригоден, чем должен быть.

Хотя многие другие ответы дают хорошие примеры того, почему вы можете захотеть перехватить исключение, похоже, никто не упомянул сценарий "наконец".

Примером этого является то, что у вас есть метод, в котором вы устанавливаете курсор (например, курсор ожидания), у метода есть несколько точек выхода (например, if () return;), и вы хотите убедиться, что курсор сброшен при конец метода.

Для этого вы можете обернуть весь код в try / catch / finally. В конце установите курсор обратно на правый курсор. Чтобы вы не хоронили никаких действительных исключений, сбросьте их в улов.

try
{
    Cursor.Current = Cursors.WaitCursor;
    // Test something
    if (testResult) return;
    // Do something else
}
catch
{
    throw;
}
finally
{
     Cursor.Current = Cursors.Default;
}

Это зависит от того, что вы делаете в блоке catch, и хотите ли вы передать ошибку в вызывающий код или нет.

Вы могли бы сказать Catch io.FileNotFoundExeption ex и затем используйте альтернативный путь к файлу или что-то подобное, но все равно выдает ошибку.

Также делаю Throw вместо Throw Ex позволяет сохранить полный след стека. Throw ex перезапускает трассировку стека из оператора throw (надеюсь, это имеет смысл).

Одной из возможных причин броска-броска является отключение любых фильтров исключений, расположенных выше в стеке ( случайная старая ссылка). Но, конечно, если бы это было намерение, там был бы комментарий, говорящий так.

В примере из кода, который вы опубликовали, фактически нет смысла перехватывать исключение, так как ничего не сделано для перехвата, который он просто повторяет, фактически он приносит больше вреда, чем пользы, так как стек вызовов потерян,

Однако вы бы поймали исключение, чтобы выполнить некоторую логику (например, закрытие sql-соединения блокировки файла или просто некоторая запись в журнал), в случае исключения сбросить его обратно в вызывающий код для обработки. Это было бы более распространенным на бизнес-уровне, чем код переднего плана, так как вы можете захотеть, чтобы кодер, реализующий ваш бизнес-уровень, обрабатывал исключение.

Повторить, хотя нет смысла поймать исключение в примере, который вы опубликовали. НЕ делай так!

Перетаскивание исключений через throw полезен, когда у вас нет определенного кода для обработки текущих исключений или в тех случаях, когда у вас есть логика для обработки определенных случаев ошибок, но вы хотите пропустить все остальные.

Пример:

string numberText = "";
try
{
    Console.Write("Enter an integer: ");
    numberText = Console.ReadLine();
    var result = int.Parse(numberText);

    Console.WriteLine("You entered {0}", result);
}
catch (FormatException)
{
    if (numberText.ToLowerInvariant() == "nothing")
    {
        Console.WriteLine("Please, please don't be lazy and enter a valid number next time.");
    }
    else
    {
        throw;
    }
}    
finally
{
    Console.WriteLine("Freed some resources.");
}
Console.ReadKey();

Однако есть и другой способ сделать это, используя условные предложения в блоках catch:

string numberText = "";
try
{
    Console.Write("Enter an integer: ");
    numberText = Console.ReadLine();
    var result = int.Parse(numberText);

    Console.WriteLine("You entered {0}", result);
}
catch (FormatException) when (numberText.ToLowerInvariant() == "nothing")
{
    Console.WriteLine("Please, please don't be lazy and enter a valid number next time.");
}    
finally
{
    Console.WriteLine("Freed some resources.");
}
Console.ReadKey();

Этот механизм более эффективен, чем повторная генерация исключения, поскольку среде выполнения.NET не нужно перестраивать объект исключения перед его повторной генерацией.

В дополнение к тому, что сказали другие, см. Мой ответ на связанный вопрос, который показывает, что перехват и перебрасывание не запрещены (это на VB, но часть кода может быть вызвана из C# из VB).

Большинство ответов говорят о сценарии catch-log-rethrow.

Вместо того, чтобы писать это в своем коде, рассмотрите возможность использования AOP, в частности Postsharp.Diagnostic.Toolkit с OnExceptionOptions IncludeParameterValue и IncludeThisArgument

Извините, но многие примеры "улучшенного дизайна" до сих пор пахнут ужасно или могут вводить в заблуждение. Попробовав { } catch { log; throw } просто совершенно бессмысленно. Регистрация исключений должна выполняться в центральном месте внутри приложения. в любом случае исключения заполняют трассировку стека, почему бы не записать их где-нибудь вверх и близко к границам системы?

Следует соблюдать осторожность при сериализации вашего контекста (например, DTO в одном из приведенных примеров) только в сообщении журнала. Он может легко содержать конфиденциальную информацию, которая может не захотеть попасть в руки всех людей, которые могут получить доступ к файлам журнала. И если вы не добавите новую информацию в исключение, я действительно не вижу смысла в исключении. У старой доброй Java есть кое-что для этого, она требует, чтобы вызывающая сторона знала, какие исключения следует ожидать, затем вызывая код. Поскольку в.NET этого нет, обертывание не принесет пользы, по крайней мере, в 80% случаев, которые я видел.

Я проголосовал за ответ eoin.

Я добавлю еще два предостережения (отличных от его ответа)

      try
{
   ...
}
catch(SQLException sqlex)
{
   /* you can optionally look at the ".Number" , but here I do not want to "leak" a database error details that talks about table-names, column-names, etc, etc */
   throw new UserFacingFriendlyException("There was a issue with the database operation", sqlex);
}
catch(ArithmeticException aex)
{    
   /* throw the exact same aex so my "handler" can deal with this specific exception type */
   throw;
}
  1. С SQLException (или SqlException !! ха-ха)... вы, возможно, не захотите, чтобы "внутренние реальные детали" этого доходили до внешнего пользователя. Таким образом, вы перехватываете и оборачиваете его как еще одно исключение. (это может быть частью оригинального ответа eoin, но я обращаю на это внимание)

или

2.

У вас есть «обработчик» (для определенного типа исключения)… где-то на верхних уровнях… который ищет определенное исключение (тип).

Если бы у вас был

https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0#Exception-filters

или собственный «IActionFilter, IOrderedFilter» (ниже URL-адреса)

https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-6.0#use-Exceptions-to-modify-the-response-1

и любой из них явно искал «ArithmeticException», тогда вам не хотелось бы «тревожить его». .

Во второй URL-статье («IActionFilter, IOrderedFilter») m$ назвал свой пример:

      HttpResponseExceptionFilter : IActionFilter, IOrderedFilter

В моем примере «перехватить ArithmeticException и выбросить» (в этом ответе).... вы могли бы написать для него явный обработчик под названием:

      ArithmeticExceptionFilter : IActionFilter, IOrderedFilter

для обработки (в частности) ArithmeticException.

.........

и если вы «объедините» две вещи выше.

UserFacingFriendlyException

Я стараюсь, чтобы моя бизнес-логика всегда выдавала это исключение.

потому что (барабанная дробь :) )

Я определил только 2

      IActionFilter, IOrderedFilter

(1)

      UserFacingFriendlyExceptionFilter : IActionFilter, IOrderedFilter

   // deal with UserFacingFriendlyExceptions ONLY. but "pass through" anything else that is not a UserFacingFriendlyException

и ~~на случай, если я что-то пропустил

(2)

      ExceptionFallThroughFilter  : IActionFilter, IOrderedFilter
    //(for anything that slipped by the UserFacingFriendlyExceptionFilter)

Таким образом... Мне гарантировано отсутствие утечки внутренних исключений.

Другие вопросы по тегам