Лучшие практики для отлова и повторного выброса исключений .NET

284

Какую наилучшую практику следует учитывать при отлове исключений и повторном их отбрасывании? Я хочу убедиться, что Exceptionобъект InnerExceptionи трассировка стека сохранены. Есть ли разница между следующими блоками кода в способе их обработки?

try
{
    //some code
}
catch (Exception ex)
{
    throw ex;
}

Vs:

try
{
    //some code
}
catch
{
    throw;
}
Seibar
источник

Ответы:

262

Способ сохранения трассировки стека заключается в использовании параметра throw;Это также верно

try {
  // something that bombs here
} catch (Exception ex)
{
    throw;
}

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

Майк также прав, предполагая, что исключение позволяет вам передать исключение (что рекомендуется).

Карл Сегин также отлично справляется с обработкой исключений в своих основах программирования электронных книг , что является отличным чтением.

Изменить: Рабочая ссылка на Основы программирования pdf. Просто найдите текст «исключение».

Даррен Копп
источник
10
Я не уверен, что эта рецензия замечательная, она предлагает попробовать {// ...} catch (Exception ex) {throw new Exception (ex.Message + "other stuff"); } это хорошо. Проблема в том, что вы совершенно не можете обработать это исключение дальше по стеку, если только вы не перехватите все исключения, большое нет-нет (вы уверены, что хотите обработать это исключение OutOfMemoryException?)
ljs
2
@ljs Изменилась ли статья с момента вашего комментария, поскольку я не вижу ни одного раздела, где он рекомендует это. Наоборот, он говорит не делать этого и спрашивает, не хотите ли вы обработать исключение OutOfMemoryException !?
RyanfaeScotland
6
Иногда бросать; недостаточно для сохранения трассировки стека. Вот пример https://dotnetfiddle.net/CkMFoX
Артавазд Балаян
4
Или ExceptionDispatchInfo.Capture(ex).Throw(); throw;в .NET +4.5 stackoverflow.com/questions/57383/…
Альфред Уоллес
Решение @AlfredWallace отлично сработало для меня. try {...} catch {throw} не сохранил трассировку стека. Спасибо.
atownson
100

Если вы сгенерируете новое исключение с начальным исключением, вы также сохраните начальную трассировку стека.

try{
} 
catch(Exception ex){
     throw new MoreDescriptiveException("here is what was happening", ex);
}
Майк
источник
Независимо от того, что я пытаюсь выбросить, новое исключение («message», ex) всегда выбрасывает ex и игнорирует пользовательское сообщение. бросить новое исключение ("message", ex.InnerException) работает, хотя.
Тод
Если пользовательское исключение не требуется, можно использовать AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/…
Никос Цокос
AggregateExceptionследует использовать только для исключений над агрегированными операциями. Например, он брошен в ParallelEnumerableи Taskклассы CLR. Использование должно, вероятно, следовать этому примеру.
Алуан Хаддад
29

На самом деле, есть некоторые ситуации, throwкогда информация не будет сохранять информацию StackTrace. Например, в коде ниже:

try
{
  int i = 0;
  int j = 12 / i; // Line 47
  int k = j + 1;
}
catch
{
  // do something
  // ...
  throw; // Line 54
}

StackTrace будет указывать, что строка 54 вызвала исключение, хотя оно было поднято в строке 47.

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at Program.WithThrowIncomplete() in Program.cs:line 54
   at Program.Main(String[] args) in Program.cs:line 106

В ситуациях, подобных описанной выше, существует два варианта сохранения исходного StackTrace:

Вызов Exception.InternalPreserveStackTrace

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

private static void PreserveStackTrace(Exception exception)
{
  MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
    BindingFlags.Instance | BindingFlags.NonPublic);
  preserveStackTrace.Invoke(exception, null);
}

У меня есть недостаток в том, что я полагаюсь на закрытый метод для сохранения информации StackTrace. Это может быть изменено в будущих версиях .NET Framework. Пример кода выше и предлагаемое решение ниже были извлечены из блога Fabrice MARGUERIE .

Вызов Exception.SetObjectData

Методика, предложенная ниже, была предложена Антоном Тихым в ответ на In C #, как я могу сбросить InnerException, не теряя вопрос трассировки стека .

static void PreserveStackTrace (Exception e) 
{ 
  var ctx = new StreamingContext  (StreamingContextStates.CrossAppDomain) ; 
  var mgr = new ObjectManager     (null, ctx) ; 
  var si  = new SerializationInfo (e.GetType (), new FormatterConverter ()) ; 

  e.GetObjectData    (si, ctx)  ; 
  mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData 
  mgr.DoFixups       ()         ; // ObjectManager calls SetObjectData 

  // voila, e is unmodified save for _remoteStackTraceString 
} 

Несмотря на то, что он имеет преимущество полагаться только на общедоступные методы, он также зависит от следующего конструктора исключений (который некоторые реализации, разработанные сторонними разработчиками, не реализуют):

protected Exception(
    SerializationInfo info,
    StreamingContext context
)

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

КАРЛОС ЛОТ
источник
1
Вы можете перехватить исключение и опубликовать это исключение в любом месте. Затем добавьте новый, объясняющий, что случилось с пользователем. Таким образом, вы можете увидеть, что произошло в настоящее время, когда исключение было обнаружено, пользователь может не обращать внимания на то, каким было действительное исключение.
Çöđěxěŕ
2
В .NET 4.5 существует третий и, на мой взгляд, более чистый вариант: используйте ExceptionDispatchInfo. См. Трагедийский ответ на связанный вопрос здесь: stackoverflow.com/a/17091351/567000 для получения дополнительной информации.
Сорен Бойсен
20

Когда вы throw ex, вы, по сути, генерируете новое исключение и пропускаете исходную информацию трассировки стека. throwявляется предпочтительным методом.

Забытая точка с запятой
источник
13

Правило большого пальца состоит в том, чтобы избегать Поймать и Бросить основной Exceptionобъект. Это заставляет вас быть немного умнее с исключениями; другими словами, у вас должен быть явный улов для a, SqlExceptionчтобы ваш код обработки не делал что-то не так с a NullReferenceException.

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

swilliams
источник
2
Я думаю, что лучше всего иметь дело с необработанными исключениями для целей ведения журнала, используя исключения AppDomain.CurrentDomain.UnhandledException и Application.ThreadException. Повсеместное использование больших блоков try {...} catch (Exception ex) {...} означает много дублирования. Зависит от того, хотите ли вы регистрировать обработанные исключения, и в этом случае (по крайней мере, минимальное) дублирование может быть неизбежным.
СМЛ
Плюс использование этих событий означает, что вы регистрируете все необработанные исключения, в то время как если вы используете блоки больших старых try {...} catch (Exception ex) {...}, вы можете пропустить некоторые из них.
СМЛ
10

Вы всегда должны использовать «бросить»; отбросить исключения в .NET,

Ссылайтесь на это, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

В основном MSIL (CIL) имеет две инструкции - «throw» и «rethrow»:

  • C # 'throw ex;' компилируется в "бросок" MSIL
  • C # 'throw;' - в MSIL "отбросить"!

По сути, я вижу причину, по которой «throw ex» переопределяет трассировку стека.

Винод Т. Патил
источник
Ссылка - ну, собственно, источник, на который ссылается эта ссылка - полна хорошей информации, а также отмечает возможного виновника того, почему многие думают, что throw ex;ее свергнут - в Java это так! Но вы должны включить эту информацию сюда, чтобы получить ответ на вопрос. (Хотя я все еще ExceptionDispatchInfo.Captureдогоняю ответ от jeuoekdcwzfwccu .)
ruffin
10

Никто не объяснил разницу между ExceptionDispatchInfo.Capture( ex ).Throw()и равниной throw, так что вот она. Однако некоторые люди заметили проблему с throw.

Полный способ отбросить перехваченное исключение - использовать ExceptionDispatchInfo.Capture( ex ).Throw()(доступно только в .Net 4.5).

Ниже приведены случаи, необходимые для проверки этого:

1.

void CallingMethod()
{
    //try
    {
        throw new Exception( "TEST" );
    }
    //catch
    {
    //    throw;
    }
}

2.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        ExceptionDispatchInfo.Capture( ex ).Throw();
        throw; // So the compiler doesn't complain about methods which don't either return or throw.
    }
}

3.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch
    {
        throw;
    }
}

4.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        throw new Exception( "RETHROW", ex );
    }
}

Случай 1 и случай 2 дадут вам трассировку стека, где номер строки исходного кода для CallingMethodметода является номером throw new Exception( "TEST" )строки строки.

Однако случай 3 даст вам трассировку стека, где номер строки исходного кода для CallingMethodметода является номером строки throwвызова. Это означает, что если throw new Exception( "TEST" )строка окружена другими операциями, вы не знаете, по какому номеру строки было выдано исключение.

Случай 4 аналогичен случаю 2, потому что номер строки исходного исключения сохраняется, но не является реальным перебросом, потому что он меняет тип исходного исключения.

jeuoekdcwzfwccu
источник
Добавьте простую рекламу, чтобы никогда не использовать, throw ex;и это лучший ответ из всех.
НХ.
8

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

Рассмотрим следующий код:

static void Main(string[] args)
{
    try
    {
        TestMe();
    }
    catch (Exception ex)
    {
        string ss = ex.ToString();
    }
}

static void TestMe()
{
    try
    {
        //here's some code that will generate an exception - line #17
    }
    catch (Exception ex)
    {
        //throw new ApplicationException(ex.ToString());
        throw ex; // line# 22
    }
}

Когда вы делаете 'throw' или 'throw ex', вы получаете трассировку стека, но строка # будет # 22, так что вы не сможете выяснить, какая именно строка выдает исключение (если у вас только 1 или несколько строки кода в блоке try). Чтобы получить ожидаемую строку № 17 в вашем исключении, вам нужно сгенерировать новое исключение с исходной трассировкой стека исключений.

notlkk
источник
3

Вы также можете использовать:

try
{
// Dangerous code
}
finally
{
// clean up, or do nothing
}

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

Эрик Б
источник
3

Я бы определенно использовал:

try
{
    //some code
}
catch
{
    //you should totally do something here, but feel free to rethrow
    //if you need to send the exception up the stack.
    throw;
}

Это сохранит ваш стек.

1kevgriff
источник
1
Честно говоря, в прошлом 2008 году меня спрашивали, как сохранить стек, а 2008 год дал правильный ответ. Чего не хватает в моем ответе, так это части того, что вы делаете в улове.
1kevgriff
@JohnSaunders Это верно тогда и только тогда, когда вы ничего не делаете раньше throw; например, вы можете очистить одноразовое устройство (когда вы вызываете его ТОЛЬКО при ошибке), а затем выбросить исключение.
Мейрион Хьюз
@ meirion, когда я написал комментарий, перед броском ничего не было. Когда это было добавлено, я проголосовал, но не удалил комментарий.
Джон Сондерс
0

К вашему сведению, я только что проверил это и трассировку стека сообщил 'throw;' не совсем корректная трассировка стека. Пример:

    private void foo()
    {
        try
        {
            bar(3);
            bar(2);
            bar(1);
            bar(0);
        }
        catch(DivideByZeroException)
        {
            //log message and rethrow...
            throw;
        }
    }

    private void bar(int b)
    {
        int a = 1;
        int c = a/b;  // Generate divide by zero exception.
    }

Трассировка стека правильно указывает на источник исключения (сообщенный номер строки), но номер строки, сообщенный для foo (), является строкой броска; оператор, следовательно, вы не можете сказать, какой из вызовов bar () вызвал исключение.

redcalx
источник
Вот почему лучше не пытаться отлавливать исключения, если вы не планируете что-то с ними делать
Нейт Заугг,