Najlepsze praktyki dotyczące wychwytywania i ponownego zgłaszania wyjątków .NET

284

Jakie najlepsze praktyki należy wziąć pod uwagę przy wychwytywaniu wyjątków i ponownym ich rzucaniu? Chcę się upewnić, że śledzenie Exceptionobiektu InnerExceptioni stosu są zachowane. Czy istnieje różnica między następującymi blokami kodu w sposobie, w jaki sobie z tym radzą?

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

Vs:

try
{
    //some code
}
catch
{
    throw;
}
Seibar
źródło

Odpowiedzi:

262

Sposobem na zachowanie śladu stosu jest również użycie opcji throw;To jest poprawne

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

throw ex;jest w zasadzie jak zgłoszenie wyjątku od tego momentu, więc ślad stosu trafiałby tylko tam, gdzie wydaje się throw ex;instrukcję.

Mike ma również rację, zakładając, że wyjątek pozwala przekazać wyjątek (co jest zalecane).

Karl Seguin ma także świetne recenzje na temat obsługi wyjątków w swoich e-bookach programistycznych , co jest świetną lekturą.

Edycja: Link roboczy do Podstawy programowania pdf. Wystarczy wyszukać w tekście „wyjątek”.

Darren Kopp
źródło
10
Nie jestem pewien, czy ten zapis jest cudowny, sugeruje spróbuj {// ...} catch (Exception ex) {wyrzuć nowy Exception (np. Wiadomość + „inne rzeczy”); } jest dobry. Problem polega na tym, że nie jesteś w stanie poradzić sobie z tym wyjątkiem, chyba że złapiesz wszystkie wyjątki, wielkie nie-nie (na pewno chcesz obsłużyć ten
wyjątek
2
@ljs Czy artykuł zmienił się od czasu twojego komentarza, ponieważ nie widzę żadnej sekcji, w której on to poleca. Wręcz przeciwnie, mówi, żeby tego nie robić i pyta, czy chcesz obsłużyć także OutOfMemoryException !?
RyanfaeScotland
6
Czasami rzucać; nie wystarczy, aby zachować ślad stosu. Oto przykład https://dotnetfiddle.net/CkMFoX
Artavazd Balayan
4
Lub ExceptionDispatchInfo.Capture(ex).Throw(); throw;w .NET +4,5 stackoverflow.com/questions/57383/...
Alfred Wallace
Rozwiązanie @AlfredWallace działało dla mnie idealnie. try {...} catch {throw} nie zachował śladu stosu. Dziękuję Ci.
atownson
100

Jeśli wyrzucisz nowy wyjątek z początkowym wyjątkiem, zachowasz również początkowy ślad stosu.

try{
} 
catch(Exception ex){
     throw new MoreDescriptiveException("here is what was happening", ex);
}
Mikrofon
źródło
Bez względu na to, co spróbuję rzucić nowy wyjątek („wiadomość”, np.) Zawsze zgłasza ex i ignoruje niestandardową wiadomość. wrzuć nowy wyjątek („wiadomość”, np. wyjątek wewnętrzny) działa.
Tod
Jeśli nie jest wymagany żaden wyjątek niestandardowy, można użyć AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/…
Nikos Tsokos
AggregateExceptionpowinien być stosowany tylko w wyjątkach od zagregowanych operacji. Na przykład jest on wyrzucany przez klasy ParallelEnumerablei TaskCLR. Użycie powinno prawdopodobnie pójść za tym przykładem.
Aluan Haddad
29

W rzeczywistości istnieją pewne sytuacje, w których throwstatystyka nie zachowa informacji StackTrace. Na przykład w poniższym kodzie:

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

StackTrace wskaże, że wiersz 54 zgłosił wyjątek, chociaż został zgłoszony w wierszu 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

W sytuacjach takich jak ta opisana powyżej istnieją dwie opcje zachowania oryginalnego StackTrace:

Wywołanie wyjątku.InternalPreserveStackTrace

Ponieważ jest to metoda prywatna, należy ją wywołać za pomocą refleksji:

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

Mam tę wadę, że polegam na prywatnej metodzie w celu zachowania informacji StackTrace. Można go zmienić w przyszłych wersjach .NET Framework. Powyższy przykład kodu i zaproponowane poniżej rozwiązanie zostało wyodrębnione z bloga internetowego Fabrice MARGUERIE .

Wywołanie wyjątku.SetObjectData

Poniższa technika została zasugerowana przez Anton Tykhyy jako odpowiedź na In C #, w jaki sposób mogę ponownie wprowadzić wyjątek wewnętrzny bez utraty pytania śledzenia stosu .

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 
} 

Chociaż ma tę zaletę, że polega wyłącznie na metodach publicznych, zależy to również od następującego konstruktora wyjątków (którego niektóre wyjątki opracowane przez strony trzecie nie implementują):

protected Exception(
    SerializationInfo info,
    StreamingContext context
)

W mojej sytuacji musiałem wybrać pierwsze podejście, ponieważ wyjątki podniesione przez bibliotekę innej firmy, z której korzystałem, nie implementowały tego konstruktora.

CARLOS LOTH
źródło
1
Możesz złapać wyjątek i opublikować go w dowolnym miejscu. Następnie rzuć nowy, wyjaśniając, co się stało z użytkownikiem. W ten sposób możesz zobaczyć, co się stało w chwili złapania wyjątku, użytkownik może nieostrożnie określić, jaki był faktyczny wyjątek.
Çöđěxěŕ
2
W .NET 4.5 istnieje trzecia i - moim zdaniem - czystsza opcja: użyj ExceptionDispatchInfo. Zobacz odpowiedź Tragedian na powiązane pytanie tutaj: stackoverflow.com/a/17091351/567000, aby uzyskać więcej informacji.
Søren Boisen,
20

Kiedy to robisz throw ex, w zasadzie rzucasz nowy wyjątek i tracisz informacje o oryginalnym stosie śledzenia. throwjest preferowaną metodą.

Zapomniany średnik
źródło
13

Zasadą jest unikanie łapania i rzucania podstawowego Exceptionobiektu. To zmusza cię do nieco mądrzejszego podejścia do wyjątków; innymi słowy powinieneś mieć wyraźny haczyk SqlException, aby twój kod obsługi nie zrobił czegoś złego z NullReferenceException.

Jednak w prawdziwym świecie łapanie i rejestrowanie wyjątku podstawowego jest również dobrą praktyką, ale nie zapomnij przejść całej rzeczy, aby uzyskać jakąkolwiek InnerExceptions.

Swilliams
źródło
2
Myślę, że najlepiej radzić sobie z nieobsługiwanymi wyjątkami do celów rejestrowania za pomocą wyjątków AppDomain.CurrentDomain.UnhandledException i Application.ThreadException. Używanie bloków typu big try {...} catch (Exception ex) {...} oznacza wszędzie dużo duplikacji. Zależy, czy chcesz rejestrować obsługiwane wyjątki, w którym to przypadku (przynajmniej minimalne) duplikowanie może być nieuniknione.
ljs
Dodatkowo korzystanie z tych zdarzeń oznacza, że rejestrujesz wszystkie nieobsługiwane wyjątki, a jeśli używasz dużych ol 'try {...} bloków catch (Exception ex) {...}, możesz przegapić niektóre.
ljs
10

Zawsze powinieneś używać „rzut”; aby ponownie wprowadzić wyjątki w .NET,

Zobacz to, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

Zasadniczo MSIL (CIL) ma dwie instrukcje - „rzut” i „rzut”:

  • C #'s „throw ex;” zostaje wkompilowany w „rzut” MSIL
  • „Rzut” C # - w „rethrow” MSIL!

Zasadniczo widzę powód, dla którego „throw ex” przesłania ślad stosu.

Vinod T. Patil
źródło
Link - cóż, właściwie źródło, które cytuje link - jest pełen dobrych informacji, a także zauważa potencjalnego winowajcę tego, dlaczego wielu myśli throw ex;, że powróci - w Javie tak! Ale musisz podać tę informację tutaj, aby uzyskać odpowiedź klasy A. (Chociaż wciąż nadrabiam zaległości ExceptionDispatchInfo.Captureodpowiedzi od jeuoekdcwzfwccu .)
ruffin
10

Nikt nie wyjaśnił różnicy między ExceptionDispatchInfo.Capture( ex ).Throw()zwykłym throw, więc oto jest. Jednak niektórzy zauważyli problem throw.

Całkowitym sposobem na ponowne wyłapanie przechwyconego wyjątku jest użycie ExceptionDispatchInfo.Capture( ex ).Throw()(dostępne tylko w .Net 4.5).

Poniżej znajdują się przypadki niezbędne do przetestowania tego:

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

Przypadek 1 i przypadek 2 dają ślad stosu, w którym numerem kodu źródłowego dla CallingMethodmetody jest numer throw new Exception( "TEST" )wiersza.

Jednak przypadek 3 da ci ślad stosu, w którym numerem linii kodu źródłowego dla CallingMethodmetody jest numer linii throwwywołania. Oznacza to, że jeśli throw new Exception( "TEST" )linia jest otoczona innymi operacjami, nie masz pojęcia, pod którym numerem linii został zgłoszony wyjątek.

Przypadek 4 jest podobny do przypadku 2, ponieważ zachowany jest numer wiersza oryginalnego wyjątku, ale nie jest to rethrow, ponieważ zmienia typ pierwotnego wyjątku.

jeuoekdcwzfwccu
źródło
Dodaj prostą tabliczkę, której nigdy nie będziesz używać, throw ex;a to najlepsza odpowiedź ze wszystkich.
NH.
8

Kilka osób faktycznie pominęło bardzo ważny punkt - „rzut” i „rzut ex” mogą zrobić to samo, ale nie dają istotnej informacji, która jest linią, w której wystąpił wyjątek.

Rozważ następujący kod:

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

Kiedy wykonujesz „rzut” lub „rzut ex”, otrzymujesz ślad stosu, ale linia # będzie miała numer 22, więc nie możesz dowiedzieć się, która linia dokładnie zgłasza wyjątek (chyba że masz tylko 1 lub kilka wiersze kodu w bloku try). Aby uzyskać oczekiwaną linię nr 17 w twoim wyjątku, musisz zgłosić nowy wyjątek z oryginalnym śledzeniem stosu wyjątków.

notlkk
źródło
3

Możesz także użyć:

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

A wszelkie zgłoszone wyjątki zwiększą się do następnego poziomu, który je obsługuje.

Erick B.
źródło
3

Zdecydowanie użyłbym:

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

To pozwoli zachować twój stos.

1kevgriff
źródło
1
Aby być uczciwym wobec mnie w 2008 roku, PO pytał, jak zachować stos - a 2008 roku udzieliłem poprawnej odpowiedzi. To, czego brakuje w mojej odpowiedzi, to część robienia czegoś w haczyku.
1kevgriff
@JohnSaunders To prawda, jeśli i tylko jeśli wcześniej nic nie robisz throw; na przykład możesz wyczyścić jednorazowe (gdzie TYLKO wywołujesz je z błędem), a następnie wyrzucić wyjątek.
Meirion Hughes
@meirion, kiedy napisałem komentarz, nie było nic przed rzutem. Kiedy to zostało dodane, głosowałem, ale nie usunąłem komentarza.
John Saunders
0

Do Twojej wiadomości Właśnie przetestowałem to i ślad stosu zgłoszony przez „throw”; nie jest całkowicie poprawnym śladem stosu. Przykład:

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

Ślad stosu poprawnie wskazuje na początek wyjątku (zgłaszany numer linii), ale numer linii zgłaszany dla foo () jest linią rzutu; instrukcja, stąd nie można powiedzieć, które z wywołań bar () spowodowało wyjątek.

redcalx
źródło
Dlatego najlepiej nie wychwytywać wyjątków, chyba że planujesz coś z nimi zrobić
Nate Zaugg