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

557

Я смотрю на статью 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).


Ссылки:

corlettk
источник
8
Хорошее резюме; дополнительные баллы за включение блока finally.
Фредрик Мёрк
Я хотел бы добавить, что вы можете использовать «бросить»; чтобы быть еще более полезным, добавив параметры, которые были отправлены методу в коллекцию e.Data перед "throw;" заявление
Майкл Бахиг
@MickTheWarMachineDesigner (и художник, работающий неполный рабочий день). А? Вы говорите об исключениях Microshite Suckwell (вероятно, начиная с 2005 года, насколько я знаю). Я говорил об обработке исключений в целом. И да, я узнал кое-что с того момента, как я опубликовал это НА ЧЕМ НА ЧЕТЫРЕ ГОДА ... Но да, я признаюсь, что у вас есть правильное мнение, но я думаю, что вы упустили истинное мнение; если ты понимаешь мой дрейф? Этот вопрос касается ОБОБЩЕННОЙ обработки исключений в C #; и более конкретно о отбрасывании исключений ... ВСЕХ видов. Круто?
Corlettk
Пожалуйста, рассмотрите возможность перемещения раздела редактирования сводки в своем вопросе на собственный ответ. Почему, смотрите Редактирование самостоятельного ответа вне вопроса и Ответа, встроенного в вопрос .
DavidRR
2
Кто-нибудь не заметил часть «Произошел экскремент»? Похоже, код пошел на какашек!
Джейсон Локи Смит

Ответы:

430

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

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

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

try 
{
    // code that may throw exceptions    
}
catch(Exception ex) 
{
    // add error logging here
    throw;
}
Фредрик Мёрк
источник
6
@ Фредрик, просто к вашему сведению (хотя вы, вероятно, знаете), если вы не собираетесь использовать этот exобъект, тогда нет необходимости создавать его экземпляр.
Эойн Кэмпбелл
78
@Eoin: Если его не создать, было бы довольно сложно его зарегистрировать.
Сэм Топор
30
Да, я думаю, что "зло" почти правильно ... рассмотрим случай исключения нулевого указателя, генерируемого где-то из большой части кода. Сообщение ванильное, без стекового следа вы остаетесь с "что-то где-то было нулевым". НЕ хорошо, когда производство мертво; и у вас НЕТ минут или меньше, чтобы решить проблему flamin ', и отклонить или исправить ее ... Хорошая обработка исключений стоит на вес золота.
Corlettk
4
Это правда и для Java ... "throw" против "throw ex"?
JasonStoltz
8
@ Джейсон, посмотри на этот вопрос . В Java throw exне перезапускается трассировка стека.
Мэтью Флэшен
117

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

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
}
Эойн Кэмпбелл
источник
7
Почему бы просто не оставить улов?
AnthonyWJones
3
по-прежнему есть смысл оставлять catch {throw; } внизу списка определенных типов исключений catch на том основании, что это доказывает, что автор рассмотрел этот случай, хотя комментария может быть достаточно. Не гадать, когда вы читаете код, это хорошо.
Аннаката
87
Почему-то имя SQLException меня беспокоит.
Майкл Майерс
13
Этот улов (Exception) {throw new Exception (...)} - это то, что вам никогда не следует делать, просто потому, что вы запутываете информацию об исключениях и делаете ненужную фильтрацию исключений в стеке вызовов излишне сложной. Единственный раз, когда вы должны перехватить один тип исключения и выбросить другой, это когда вы реализуете уровень абстракции и вам нужно преобразовать тип исключения для конкретного поставщика (например, SqlException вместо XmlException) в более общий (например, DataLoadingException).
jammycakes
3
@dark_perfect, вы должны проверить этот аргумент заранее, в начале метода и выбросить исключение ArgumentNullException (быстро потерпеть неудачу).
Андрей Бозантан
56

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
}
bzlm
источник
2
Дейв, но (по крайней мере, в java) вы бы не генерировали «универсальное» исключение MyException, вы бы определяли СПЕЦИФИЧЕСКИЙ тип исключения и выбрасывали его, позволяя дифференцировать его по типу в блоке catch ... Но да , если вы не являетесь архитектором исключения (я думаю, здесь SQLException JDBC (снова Java), который является отвратительно родовым и предоставляет метод getErrorCode () ... Хммм ... У вас есть точка зрения, это просто Я думаю, что есть лучший способ сделать это, где это возможно. Приветствия, приятель. Я очень ценю ваше время. Кит.
corlettk
1
Ну, вопрос «Зачем ловить и перебрасывать исключения в C #?», И это ответ. =] ... и даже со специализированными исключениями, фильтры исключений имеют смысл: рассмотрим случай, когда вы, скажем, обрабатываете SqlTimeoutException и SqlConnectionResetException, которые оба являются SqlException. Фильтры исключений позволяют вам перехватывать SqlException только тогда, когда он является одним из этих двух, поэтому вместо того, чтобы загромождать попытку / перехват идентичной обработкой для этих двух, вы можете «перехватить SqlException ex, когда ex - это SqlTimeoutException или ex - это SqlConnectionResetException». (Я не Дэйв, кстати)
bzlm
3
Отфильтрованные исключения идут в C # 6!
Сэр Криспалот
14

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

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

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

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

Питер Мортенсен
источник
1
Да, я заплачу за это, но да, вы не хотели бы видеть это в опубликованном коде ... ergo: мне было бы стыдно публиковать его ;-)
corlettk
25
На самом деле, в этом нет необходимости - в Visual Studio вы можете настроить прерывание отладчика при возникновении исключения, и оно отобразит детали исключения в окне инспектора.
jammycakes
8
Если вы хотите использовать какой-то код ТОЛЬКО во время отладки, используйте #if DEBUG ... #endif, и вам не нужно удалять эти строки
Майкл Фрейдгейм
1
Я сделал это несколько раз сам. Время от времени каждый сбежит к выпуску. @jammycakes Проблема с разрывом исключений в Visual Studio заключается в том, что иногда я хочу, чтобы выбрасываемое исключение было не единственным (или даже только одним из его типов). Все еще не знаю условия точки останова с «break, если пропущено по исключению». До тех пор это останется полезным. Майкл Фрейдгейм: #if DEBUGвокруг ОБАtry { и } catch () {...}немного грязный и, честно говоря, делает меня тошнило ... Предварительно процессор, вообще говоря, не мой друг.
11

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

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);
  }
}
edosoft
источник
Спасибо, да, упаковка исключений (особенно цепочка) совершенно вменяема ... то, что не вменяемое, - это исключение только для того, чтобы вы могли убрать трассировку стека или, что еще хуже, съесть ее.
Corlettk
10

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

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

Арджан Эйнбу
источник
8

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

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

Дункан
источник
Вы не теряете весь стек вызовов при использовании throw ex, вы просто теряете часть стека вызовов с той точки, где исключение возникло выше его стека вызовов. Но вы сохраняете стек вызовов из метода, который выбросил исключение, туда, где его вызвал клиент. На самом деле могут быть случаи, когда вы будете использовать это, иначе хорошие люди в Microsoft не допустили бы этого. Тем не менее, я не использовал его. Еще одна проблема, которую нужно помнить, это то, что бросать исключения стоит дорого. Делайте это только по очень оправданной причине. Ведение журнала, я думаю, будет оправданным и т. Д.
Чарльз Оуэн
5

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

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

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

Supercat
источник
5

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

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

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

Джексон Тариса
источник
3

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

Брайан
источник
Я не понял, о чем вы, пока не прочитал ссылку ... и я все еще не совсем уверен, о чем вы ... я совершенно незнаком с VB.NET. Я думаю, это приводит к тому, что сумма сообщается как «несовместимая», верно? ... Я БОЛЬШОЙ фанат статических методов ... Помимо простоты, вероятность несоответствия меньше, если вы отделите установку атрибутов от кода которая делает реальную работу. Стек "самоочищающийся".
Corlettk
3
Люди ожидают, что когда они пишут "try {Foo ();} finally {Bar ();}"), между Foo и Bar ничего не происходит. Но это не правда; если ваш вызывающий добавил фильтр исключений, и нет промежуточных 'catch' и бросков Foo (), то какой-то другой случайный код вашего вызывающего абонента будет запущен до того, как будет запущен finally (Bar). Это очень плохо, если вы нарушили инварианты или повысили уровень безопасности, ожидая, что они будут «немедленно» восстановлены до нормального значения методом finally, и никакой другой код не увидит временное изменение.
Брайан
3

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

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

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

Pondidum
источник
3

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

Примером этого является то, что у вас есть метод, в котором вы устанавливаете курсор (например, курсор ожидания), у метода есть несколько точек выхода (например, 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;
}
Statler
источник
1
Была catchли обязательная часть try...finallyисторически или она играет функциональную роль в этом примере? - Я просто дважды проверил, и я могу использовать try {} finally {}вообще без блока catch.
Себи
2

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

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

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

Sheff
источник
1

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

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


источник
Спасибо за твою мысль, Джо. В Java (и C #, я полагаю) я хотел бы видеть аннотацию на уровне класса @FaultBoundary, которая заставляет ВСЕ исключения (включая непроверенные типы исключений) быть пойманными или объявленными как выброшенные. Я бы использовал эту аннотацию на открытых интерфейсах каждого архитектурного слоя. Таким образом, интерфейс @FaultBoundary ThingDAO не сможет пропускать детали реализации, такие как SQLExceptions, NPE или AIOB. Вместо этого будет записана «причинная» трассировка стека и будет сгенерировано исключение DAOSystemException ... Я определяю системное исключение как «постоянно фатальное».
Corlettk
5
Есть много причин, чтобы поймать, войти, а затем сбросить. В частности, если метод с журналом перехвата содержит информацию, которую вы теряете, когда выходите из метода. Ошибка может быть обработана позже, но не записана, и вы потеряли информацию о дефектах в системе.
Энди
1
Именно здесь удобно использовать свойство Data класса Exception - захват всей этой локальной информации для общей регистрации. Первоначально эта статья привлекла мое внимание: blog.abodit.com/2010/03/…
McGuireV10
1

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

erikkallen
источник
Хотя эта ссылка может ответить на вопрос, лучше включить сюда основные части ответа и предоставить ссылку для справки. Ответы, содержащие только ссылки, могут стать недействительными, если связанная страница изменится. - Из обзора
LHIOUI
@HamzaLH, я согласен, что это не очень хорошо написанный ответ, но в нем есть информация, отличная от других ответов и положительных голосов. Поэтому я не понимаю, почему вы предлагаете удалить его? «Короткие ответы по теме, которые дают решение, остаются ответами». От meta.stackexchange.com/questions/226258/…
Майкл Фрейдгейм
это только для ссылки ответ
LHIOUI
1. Ответы только на ссылки должны быть изменены на комментарии, а не удалены. 2. Это ссылка на другой вопрос SO, а не на внешний сайт, который считается менее вероятным с течением времени. 3. У него есть дополнительное описание, которое делает его «не только ссылкой» - см. Meta.stackexchange.com/questions/225370/…
Майкл Фрейдгейм,
1

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

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

Майкл Фрейдгейм
источник
Хотя эта ссылка может ответить на вопрос, лучше включить сюда основные части ответа и предоставить ссылку для справки. Ответы, содержащие только ссылки, могут стать недействительными, если связанная страница изменится. - Из обзора
Тони Донг
@ ТониДонг, я согласен, что это не очень хорошо написанный ответ, но в нем есть информация, отличная от других ответов и положительных голосов. Поэтому я не понимаю, почему вы предлагаете удалить его? Кстати, ссылка через 5 лет остается в силе. «Короткие ответы по теме, которые дают решение, остаются ответами». От meta.stackexchange.com/questions/226258/…
Майкл Фрейдгейм
Stackoverflow имеет только это предложение.
Тони Донг
@TonyDong, если ответ не является абсолютно бесполезным, вы должны выбрать «Выглядит хорошо»
Майкл Фрейдгейм
0

Возврат исключений через 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 не должна перестраивать объект исключения перед его повторным вызовом.

Арсен Хачатурян
источник