关于.net:如何重新抛出InnerException而不会丢失C#中的堆栈跟踪?

关于.net:如何重新抛出InnerException而不会丢失C#中的堆栈跟踪?

How to rethrow InnerException without losing stack trace in C#?

我正在通过反射调用可能导致异常的方法。如果没有包装反射,如何将异常传递给调用方?我正在重新处理innerException,但这会破坏堆栈跟踪。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
    }
}

在.NET 4.5中,现在有了ExceptionDispatchInfo类。

这允许您捕获一个异常并在不更改堆栈跟踪的情况下重新抛出它:

1
2
3
4
5
6
7
8
try
{
    task.Wait();
}
catch(AggregateException ex)
{
    ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}

这适用于任何例外情况,而不仅仅是AggregateException

它是由于awaitc语言特性而引入的,它从AggregateException实例中解包了内部异常,以使异步语言特性更像同步语言特性。


可以在不反射的情况下重新刷新之前保留堆栈跟踪:

1
2
3
4
5
6
7
8
9
10
11
12
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相比,这浪费了很多周期,但具有仅依赖公共功能的优势。以下是堆栈跟踪保留函数的几种常见使用模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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 ;
}


我想你最好的办法就是把这个放在你的拦网里:

1
throw;

稍后提取神经感觉。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 );
    }
}

在抛出异常之前对其调用扩展方法,它将保留原始堆栈跟踪。


更多的思考…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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的讨论。


没有人解释过ExceptionDispatchInfo.Capture( ex ).Throw()和普通throw之间的区别,所以这里是。

重新引发捕获的异常的完整方法是使用ExceptionDispatchInfo.Capture( ex ).Throw()(仅可从.NET 4.5获得)。

下面是测试这一点的必要案例:

1。

1
2
3
4
5
6
7
8
9
10
11
void CallingMethod()
{
    //try
    {
        throw new Exception("TEST" );
    }
    //catch
    {
    //    throw;
    }
}

2。

1
2
3
4
5
6
7
8
9
10
11
12
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.
    }
}

三。

1
2
3
4
5
6
7
8
9
10
11
void CallingMethod()
{
    try
    {
        throw new Exception("TEST" );
    }
    catch
    {
        throw;
    }
}

4。

1
2
3
4
5
6
7
8
9
10
11
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类似,因为保留了原始异常的行号,但由于它更改了原始异常的类型,因此不是真正的重新引发。


第一:不要丢失TargetInvocationException——当您想要调试东西时,它是很有价值的信息。第二:在您自己的异常类型中将领带包装为innerException,并放置一个链接到所需内容的originalException属性(并保持整个调用堆栈完整)。第三:让你的方法失效。


伙计们,你们很酷……我很快就会成为一名巫师。

1
2
3
4
5
6
7
8
9
10
11
12
    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))();

    }

使用异常序列化/反序列化的其他示例代码。它不要求实际的异常类型可序列化。它也只使用公共/受保护的方法。

1
2
3
4
5
6
7
8
9
    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 });
    }

推荐阅读