Найкращі практики вилучення та повторного викидання .NET винятків


284

Які найкращі практики слід враховувати під час вилучення винятків та їх повторного скидання? Я хочу переконатися, що слід і Exceptionоб'єкт InnerExceptionстека збереглися. Чи є різниця між наступними блоками коду в тому, як вони справляються з цим?

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

Vs:

try
{
    //some code
}
catch
{
    throw;
}

Відповіді:


262

Спосіб зберегти трасування стека через використання throw;Це дійсно так

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

throw ex;в основному як викинути виняток з цього пункту, тому слід стека буде йти лише туди, де ви видаєте throw ex;заяву.

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

Карл Сегуїн також чудово пише про обробку винятків у своїх основах програмування електронної книги , що є чудовим читанням.

Редагувати: Робоче посилання на Основи програмування pdf. Просто шукайте текст "виняток".


10
Я не дуже впевнений, що це записування чудово, я пропоную спробувати {// ...} catch (Exception ex) {кинути новий виняток (ex.Message + "інші речі"); } це добре. Проблема полягає в тому, що ви повністю не в змозі обробляти цей виняток в будь-якій подальшій версії стека, якщо ви не знайдете всі винятки, великий ні-ні (ви впевнені, що хочете обробити цей 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 для мене прекрасно працювало. спробуйте {...} catch {кидок} не зберег слід сліду. Дякую.
atownson

100

Якщо ви кинете новий виняток із початковим винятком, ви збережете і початковий слід стека.

try{
} 
catch(Exception ex){
     throw new MoreDescriptiveException("here is what was happening", ex);
}

Незалежно від того, що я намагаюся кинути новий виняток ("повідомлення", колишній), завжди викидає колишній і ігнорує користувацьке повідомлення. кинути новий виняток ("повідомлення", ex.InnerException) працює, хоча.
Тод

Якщо не потрібен спеціальний виняток, можна використовувати AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/…
Нікос Цокос

AggregateExceptionслід використовувати лише для виключень із сукупних операцій. Наприклад, це викидається класами CLR ParallelEnumerableта Taskкласами. Використання, ймовірно, слід за цим прикладом.
Алуан Хаддад

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:

Виклик винятку.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 .

Виклик винятку.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 для отримання додаткової інформації.
Søren Boisen

20

Коли ви throw ex, ви, по суті, кидаєте новий виняток і будете пропускати оригінальну інформацію про сліди стека. throwє кращим способом.


13

Головне правило - уникати лову та метання основного Exceptionоб'єкта. Це змушує вас бути трохи розумнішими щодо винятків; іншими словами, у вас повинен бути чіткий улов для SqlExceptionтого, щоб ваш код обробки не робив щось не так з a NullReferenceException.

У реальному світі, хоча, вилов та реєстрація базового винятку також є хорошою практикою, але не забудьте пройтися цілою справою, щоб отримати будь-яке, InnerExceptionsщо може бути.


2
Я думаю, що найкраще боротися з необробленими винятками для цілей реєстрації, використовуючи винятки AppDomain.CurrentDomain.UnhandledException та Application.ThreadException. Використання великих блоків {...} catch (Exception ex) {...} скрізь означає багато дублювання. Залежить від того, чи хочете ви реєструвати оброблювані винятки, і в цьому випадку (принаймні мінімальне) дублювання може бути неминучим.
ljs

Крім того, використовуючи ці події означають , що ви робите реєструвати всі необроблені виключення, в той час як якщо ви використовуєте великий Ол »спробувати {...} спіймати (Exception ех) {...} блоки ви можете пропустити деякі з них .
ljs

10

Ви завжди повинні використовувати "кинути"; повторно скинути винятки в .NET,

Посилайтеся на це, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

В основному MSIL (CIL) має дві інструкції - "кинути" і "перекинути":

  • C # 's "кинути екс;" збирається в "кидок" MSIL
  • C # 's "кинути;" - в MSIL "переробити"!

В основному я бачу причину, чому "кинути екс" переосмислює слід стека.


Посилання - ну, власне, джерело, на яке посилається посилання, - переповнене хорошою інформацією, а також зазначає можливого винуватця того, чому багато хто думає 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, оскільки номер рядка вихідного винятку зберігається, але не є реальним перезарядкою, оскільки він змінює тип вихідного винятку.


Додайте просту розмитості ніколи не використовувати, throw ex;і це найкраща відповідь на них усіх.
NH.

8

Кілька людей насправді пропустили дуже важливий момент - «кинути» та «кинути екс» можуть зробити те саме, але вони не дають тобі важливої ​​інформації, яка є лінією, де сталося виняток.

Розглянемо наступний код:

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

Коли ви робите або "кинути", або "кинути колишнє", ви отримуєте слід стека, але рядок №2 буде # 22, тому ви не можете зрозуміти, який рядок точно кидав виняток (якщо у вас є лише 1 або кілька рядки коду в блоці спробу). Щоб отримати очікуваний рядок №17 у своєму винятку, вам доведеться викинути новий виняток з оригінальним слідом стека винятків.


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

Це збереже ваш стек.


1
Справедливості до минулого в 2008 році, ОП запитувала, як зберегти стек - і в 2008 році я дав правильну відповідь. Чого не вистачає в моїй відповіді, це частина того, що насправді щось робити в улові.
1kevgriff

@JohnSaunders Це правда, якщо і тільки якщо ви нічого не робите раніше throw; наприклад, ви можете очистити одноразові (де ви ТИЛЬКО називаєте це помилкою), а потім кинути виняток.
Meirion Hughes

@meirion, коли я писав коментар, перед кидком нічого не було. Коли це було додано, я підтримав, але коментар не видалив.
Джон Сондерс

0

FYI Я щойно перевірив це і слід стека, про який повідомляється "кинути"; не є абсолютно правильним слідом стека. Приклад:

    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 (), є рядком кидка; заява, отже, ви не можете сказати, який із дзвінків на бар () викликав виняток.


Ось чому найкраще не намагатися ловити винятки, якщо ви не плануєте щось з ними робити
Nate Zaugg
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.