Зачем ловить и реконструировать исключение в С#?

Я смотрю на статью С# - объект передачи данных на сериализуемые 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;
    }
}

Остальная часть статьи выглядит разумной и разумной (для noob), но этот 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();
}

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

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


EDIT:

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

НЕ

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

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

DO

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).


Литература:

Ответ 1

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

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

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

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

Ответ 2

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

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
}

Ответ 3

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

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

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

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

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

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

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

Ответ 4

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

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);
  }
}

Ответ 5

Моя основная причина наличия кода:

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

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

Однако они приятны во время отладки.

Ответ 6

Разве это не эквивалентно Обработка исключений вообще?

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

Ответ 7

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

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

Ответ 8

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

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

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

Ответ 9

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

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

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

Ответ 10

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

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

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

Ответ 11

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

Ответ 12

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

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

Ответ 13

В дополнение к тому, что говорили другие, см. мой ответ на связанный с ним вопрос, который показывает, что улавливание и реинтронирование не являются no-op (это в VB, но часть кода может быть вызвана С# из VB).

Ответ 14

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

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

Ответ 15

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

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

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

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