Jak ponownie zgłosić wyjątek wewnętrzny bez utraty śladu stosu w języku C #?

305

Wzywam, poprzez refleksję, metodę, która może powodować wyjątek. Jak mogę przekazać wyjątek mojemu rozmówcy bez otaczania go odbiciem opakowania?
Powtarzam wyjątek wewnętrzny, ale to niszczy ślad stosu.
Przykładowy kod:

public void test1()
{
    // Throw an exception for testing purposes
    throw new ArgumentException("test1");
}

void test2()
{
    try
    {
        MethodInfo mi = typeof(Program).GetMethod("test1");
        mi.Invoke(this, null);
    }
    catch (TargetInvocationException tiex)
    {
        // Throw the new exception
        throw tiex.InnerException;
    }
}
skolima
źródło
1
Jest inny sposób na zrobienie tego, który nie wymaga żadnego voodoo. Spójrz na odpowiedź tutaj: stackoverflow.com/questions/15668334/…
Timothy Shields
Wyjątek zgłoszony w metodzie nazywanej dynamicznie jest wewnętrznym wyjątkiem wyjątku „Wyjątek został zgłoszony przez cel wywołania”. Ma własny ślad stosu. Naprawdę nie ma się czym martwić.
ajeh

Odpowiedzi:

481

W .NET 4.5 jest teraz ExceptionDispatchInfoklasa.

Pozwala to uchwycić wyjątek i ponownie go wyrzucić bez zmiany śledzenia stosu:

try
{
    task.Wait();
}
catch(AggregateException ex)
{
    ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}

Działa to na każdym wyjątku, nie tylko AggregateException.

Został on wprowadzony dzięki awaitfunkcji języka C #, która rozpakowuje wewnętrzne wyjątki od AggregateExceptioninstancji, aby asynchroniczne funkcje języka bardziej przypominały funkcje języka synchronicznego.

Paul Turner
źródło
11
Dobry kandydat na metodę rozszerzenia Exception.Rethrow ()?
nmarler
8
Należy zauważyć, że klasa ExceptionDispatchInfo znajduje się w przestrzeni nazw System.Runtime.ExceptionServices i nie jest dostępna przed wersją .NET 4.5.
yoyo
53
Może być konieczne wstawienie regularnego throw;po wierszu .Throw (), ponieważ kompilator nie będzie wiedział, że .Throw () zawsze zgłasza wyjątek. throw;nigdy nie zostanie wywołany, ale przynajmniej kompilator nie będzie narzekał, jeśli twoja metoda wymaga obiektu zwrotnego lub jest funkcją asynchroniczną.
Todd
5
@Taudris To pytanie dotyczy w szczególności ponownego wprowadzenia wewnętrznego wyjątku, którego nie można rozwiązać specjalnie throw;. Jeśli użyjesz throw ex.InnerException;śledzenia stosu, zostanie on ponownie zainicjowany w punkcie, w którym zostanie ponownie wyrzucony.
Paul Turner,
5
@amitjhaExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
Vedran
86

Możliwe jest zachowanie śladu stosu przed ponownym rzuceniem bez odbicia:

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
}

To marnuje wiele cykli w porównaniu do dzwonienia InternalPreserveStackTraceprzez buforowanego delegata, ale ma tę zaletę, że polega tylko na funkcjonalności publicznej. Oto kilka typowych wzorców użycia funkcji zachowywania śledzenia stosu:

// usage (A): cross-thread invoke, messaging, custom task schedulers etc.
catch (Exception e)
{
    PreserveStackTrace (e) ;

    // store exception to be re-thrown later,
    // possibly in a different thread
    operationResult.Exception = e ;
}

// usage (B): after calling MethodInfo.Invoke() and the like
catch (TargetInvocationException tiex)
{
    PreserveStackTrace (tiex.InnerException) ;

    // unwrap TargetInvocationException, so that typed catch clauses 
    // in library/3rd-party code can work correctly;
    // new stack trace is appended to existing one
    throw tiex.InnerException ;
}
Anton Tykhyy
źródło
Wygląda świetnie, co musi się stać po uruchomieniu tych funkcji?
vdboor
2
W rzeczywistości nie jest to dużo wolniejsze niż wywoływanie InternalPreserveStackTrace(około 6% wolniej przy 10000 iteracjach). Bezpośredni dostęp do pól za pomocą refleksji jest około 2,5% szybszy niż wywoływanieInternalPreserveStackTrace
Thomas Levesque
1
Polecam używanie e.Datasłownika z ciągiem znaków lub unikalnym kluczem obiektowym ( static readonly object myExceptionDataKey = new object ()ale nie rób tego, jeśli musisz serializować wyjątki w dowolnym miejscu). Unikaj modyfikacji e.Message, ponieważ możesz mieć kod gdzieś, który analizuje e.Message. Parsowanie e.Messagejest złe, ale może nie być innego wyboru, np. Jeśli musisz użyć biblioteki innej firmy ze złymi praktykami wyjątkowymi.
Anton Tykhyy
10
DoFixups zrywa z powodu niestandardowych wyjątków, jeśli nie mają one serializatora
ruslander
3
Sugerowane rozwiązanie nie działa, jeśli wyjątek nie ma konstruktora serializacji. Proponuję skorzystać z rozwiązania zaproponowanego w stackoverflow.com/a/4557183/209727, które działa dobrze w każdym przypadku. W przypadku .NET 4.5 rozważ użycie klasy ExceptionDispatchInfo.
Davide Icardi
33

Myślę, że najlepszym rozwiązaniem byłoby po prostu umieszczenie tego w bloku catch:

throw;

A później wyodrębnij innerexception.

GEOCHET
źródło
21
Lub całkowicie usuń try / catch.
Daniel Earwicker,
6
@Earwicker. Usunięcie try / catch nie jest ogólnie dobrym rozwiązaniem, ponieważ ignoruje przypadki, w których wymagany jest kod czyszczenia przed propagacją wyjątku na stosie wywołań.
Jordan
12
@Jordan - Kod czyszczenia powinien znajdować się w bloku, a nie w bloku catch
Paolo
17
@Paolo - Jeśli ma być wykonywane w każdym przypadku, tak. Jeśli ma być wykonywany tylko w przypadku awarii, nie.
chiccodoro,
4
Pamiętaj, że InternalPreserveStackTrace nie jest bezpieczny dla wątków, więc jeśli masz 2 wątki w tych wyjątkowych stanach ... niech Bóg zlituje się nad nami wszystkimi.
Rob
13
public static class ExceptionHelper
{
    private static Action<Exception> _preserveInternalException;

    static ExceptionHelper()
    {
        MethodInfo preserveStackTrace = typeof( Exception ).GetMethod( "InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic );
        _preserveInternalException = (Action<Exception>)Delegate.CreateDelegate( typeof( Action<Exception> ), preserveStackTrace );            
    }

    public static void PreserveStackTrace( this Exception ex )
    {
        _preserveInternalException( ex );
    }
}

Wywołaj metodę rozszerzenia wyjątku, zanim ją wyrzucisz, zachowa oryginalny ślad stosu.

Eric
źródło
Pamiętaj, że w .Net 4.0 InternalPreserveStackTrace nie jest już dostępny - spójrz w Reflector, a zobaczysz, że metoda jest całkowicie pusta!
Samuel Jack
Zadrapanie: Patrzyłem na RC: w wersji beta ponownie wprowadzili implementację!
Samuel Jack
3
sugestia: zmień PreserveStackTrace, aby zwrócić ex - a następnie, aby zgłosić wyjątek, możesz po prostu powiedzieć: throw ex.PreserveStackTrace ();
Simon_Weaver
Dlaczego warto korzystać Action<Exception>? Tutaj zastosuj metodę statyczną
Kiquenet
13

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

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 oryginalnego wyjątku.

jeuoekdcwzfwccu
źródło
4
Zawsze myślałem, że „rzut” nie resetuje śledzenia stosu (w przeciwieństwie do „rzutu e”).
Jesper Matthiesen
@JesperMatthiesen Mogę się mylić, ale słyszałem, że to zależy, czy wyjątek zostanie zgłoszony i przechwycony w tym samym pliku. Jeśli jest to ten sam plik, ślad stosu zostanie utracony, jeśli jest to inny plik, zostanie zachowany.
jahu
10

Jeszcze więcej refleksji ...

catch (TargetInvocationException tiex)
{
    // Get the _remoteStackTraceString of the Exception class
    FieldInfo remoteStackTraceString = typeof(Exception)
        .GetField("_remoteStackTraceString",
            BindingFlags.Instance | BindingFlags.NonPublic); // MS.Net

    if (remoteStackTraceString == null)
        remoteStackTraceString = typeof(Exception)
        .GetField("remote_stack_trace",
            BindingFlags.Instance | BindingFlags.NonPublic); // Mono

    // Set the InnerException._remoteStackTraceString
    // to the current InnerException.StackTrace
    remoteStackTraceString.SetValue(tiex.InnerException,
        tiex.InnerException.StackTrace + Environment.NewLine);

    // Throw the new exception
    throw tiex.InnerException;
}

Należy pamiętać, że może to nastąpić w dowolnym momencie, ponieważ pola prywatne nie są częścią API. Zobacz dalszą dyskusję na temat Bugzilli Mono .

skolima
źródło
28
To naprawdę zły pomysł, ponieważ zależy od wewnętrznych nieudokumentowanych szczegółów na temat klas frameworka.
Daniel Earwicker,
1
Okazuje się, że można zachować ślad stosu bez Odbicia, patrz poniżej.
Anton Tykhyy,
1
Wywołanie InternalPreserveStackTracemetody wewnętrznej byłoby lepsze, ponieważ robi to samo i jest mniej prawdopodobne, że zmieni się w przyszłości ...
Thomas Levesque
1
W rzeczywistości byłoby gorzej, ponieważ InternalPreserveStackTrace nie istnieje na Mono.
skolima
5
@ Daniel - cóż, to naprawdę bardzo zły pomysł na rzut; aby zresetować stacktrace, gdy każdy programista .net zostanie przeszkolony, aby uwierzyć, że tak nie będzie. jest to również bardzo, naprawdę, bardzo zła rzecz, jeśli nie możesz znaleźć źródła wyjątku NullReferenceException i stracić klienta / zamówienie, ponieważ nie możesz go znaleźć. dla mnie przebija „nieudokumentowane szczegóły” i zdecydowanie mono.
Simon_Weaver
10

Po pierwsze: nie trać wyjątku TargetInvocationException - jest to cenna informacja, kiedy będziesz chciał debugować różne rzeczy.
Po drugie: Zawiń TIE jako InnerException we własnym typie wyjątku i umieść właściwość OriginalException, która łączy się z tym, czego potrzebujesz (i zachowaj cały stos połączeń nienaruszony).
Po trzecie: pozwól TIE wygasnąć z twojej metody.

kokos
źródło
5

Chłopaki, jesteście fajni .. Niedługo będę nekromantą.

    public void test1()
    {
        // Throw an exception for testing purposes
        throw new ArgumentException("test1");
    }

    void test2()
    {
            MethodInfo mi = typeof(Program).GetMethod("test1");
            ((Action)Delegate.CreateDelegate(typeof(Action), mi))();

    }
Boris Treukhov
źródło
1
Fajny pomysł, ale nie zawsze kontrolujesz kod, który wywołuje .Invoke().
Anton Tykhyy,
1
I nie zawsze znasz typy argumentów / wyników w czasie kompilacji.
Roman Starkov
3

Kolejny przykładowy kod, który wykorzystuje serializację / deserializację wyjątków. Nie wymaga faktycznego typu wyjątku, aby można go serializować. Używa także tylko metod publicznych / chronionych.

    static void PreserveStackTrace(Exception e)
    {
        var ctx = new StreamingContext(StreamingContextStates.CrossAppDomain);
        var si = new SerializationInfo(typeof(Exception), new FormatterConverter());
        var ctor = typeof(Exception).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(SerializationInfo), typeof(StreamingContext) }, null);

        e.GetObjectData(si, ctx);
        ctor.Invoke(e, new object[] { si, ctx });
    }
produkt z kurczaka
źródło
nie wymaga faktycznego typu wyjątku, aby można go serializować?
Kiquenet,
3

Na podstawie odpowiedzi Paula Turnersa opracowałem metodę rozszerzenia

    public static Exception Capture(this Exception ex)
    {
        ExceptionDispatchInfo.Capture(ex).Throw();
        return ex;
    }

return exist nigdy nie osiągnął, ale zaletą jest to, że można używać throw ex.Capture()jako jedną wkładką więc kompilator nie zgłosi not all code paths return a valuebłąd.

    public static object InvokeEx(this MethodInfo method, object obj, object[] parameters)
    {
        {
            return method.Invoke(obj, parameters);
        }
        catch (TargetInvocationException ex) when (ex.InnerException != null)
        {
            throw ex.InnerException.Capture();
        }
    }
Jürgen Steinblock
źródło