Как сбросить InnerException без потери трассировки стека в C #? - PullRequest
268 голосов
/ 11 сентября 2008

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

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

Ответы [ 9 ]

418 голосов
/ 13 июня 2013

В .NET 4.5 теперь существует класс ExceptionDispatchInfo.

Это позволяет вам захватить исключение и повторно выдать его без изменения трассировки стека:

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

Это работает для любого исключения, а не только для AggregateException.

Он появился благодаря функции языка await C #, которая разворачивает внутренние исключения из экземпляров AggregateException, чтобы сделать функции асинхронного языка более похожими на функции синхронного языка.

85 голосов
/ 18 января 2010

В можно можно сохранить трассировку стека до повторного броска без отражения:

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
}

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

// 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 ;
}
31 голосов
/ 11 сентября 2008

Я думаю, что вам лучше всего поставить это в блоке улова:

throw;

А потом извлеките исключение.

12 голосов
/ 02 ноября 2009
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 );
    }
}

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

11 голосов
/ 11 сентября 2008

Еще больше размышлений ...

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

Имейте в виду, что это может сломаться в любое время, поскольку частные поля не являются частью API. Смотрите дальнейшее обсуждение на Mono bugzilla .

10 голосов
/ 14 ноября 2016

Никто не объяснил разницу между ExceptionDispatchInfo.Capture( ex ).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, поскольку номер строки исходного исключения сохраняется, но не является реальным перебрасыванием, поскольку он меняет тип исходного исключения.

10 голосов
/ 11 сентября 2008

Первое: не теряйте исключение TargetInvocationException - это ценная информация, когда вы захотите отладить вещи.
Второе: оберните TIE как InnerException в свой собственный тип исключения и поместите свойство OriginalException, которое ссылается на то, что вам нужно (и сохраните весь стек вызовов без изменений). В-третьих: пусть TIE лопнет из вашего метода.

5 голосов
/ 02 января 2010

Ребята, вы крутые .. Я скоро стану некромантом.

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

    }
3 голосов
/ 03 апреля 2012

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

    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 });
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...