如何在不丢失C#中的堆栈跟踪的情况下抛出InnerException?


305

我通过反射调用可能导致异常的方法。在没有包装反射的情况下,如何将异常传递给调用者?
我抛出了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;
    }
}

1
还有另一种方法,不需要任何伏都教徒。看看答案在这里:stackoverflow.com/questions/15668334/...
蒂莫西·希尔兹

动态调用的方法中引发的异常是“调用的目标已引发异常”异常的内部异常。它具有自己的堆栈跟踪。确实没有什么可担心的。
ajeh

Answers:


481

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

这使您可以捕获异常并重新引发它,而无需更改堆栈跟踪:

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

这适用于任何异常,而不仅仅是AggregateException

它是由于awaitC#语言功能而引入的,该功能从AggregateException实例解开了内部异常,以使异步语言功能更像同步语言功能。


11
Exception.Rethrow()扩展方法的好候选者吗?
nmarler 2014年

8
请注意,ExceptionDispatchInfo类位于System.Runtime.ExceptionServices命名空间中,并且在.NET 4.5之前不可用。
2014年

53
您可能需要throw;在.Throw()行之后放置一个常规字符,因为编译器不会知道.Throw()总是会引发异常。throw;永远不会因此而被调用,但是如果您的方法需要返回对象或是异步函数,至少编译器不会抱怨。
2014年

5
@Taudris这个问题是专门关于重新抛出内部异常的,该异常不能由处理throw;。如果使用throw ex.InnerException;堆栈跟踪,则会在重新抛出该点时对其进行初始化。
Paul Turner

5
@amitjhaExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
Vedran

86

可能的,而不反射重新抛出之前保存堆栈跟踪:

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

看起来很酷,运行这些功能后需要做什么?
vdboor 2010年

2
实际上,它并不比调用慢很多InternalPreserveStackTrace(10000次迭代大约慢6%)。通过反射直接访问字段比调用大约快2.5%InternalPreserveStackTrace
Thomas Levesque 2010年

1
我建议将e.Data字典与字符串或唯一的对象键一起使用(static readonly object myExceptionDataKey = new object (),但是如果必须在任何地方序列化异常,则不要这样做)。避免进行修改e.Message,因为您可能在某个地方可以解析代码e.Message。解析e.Message是邪恶的,但可能别无选择,例如,如果您必须使用异常实践不佳的第三方库。
Anton Tykhyy,2010年

10
如果DoFixups没有序列化ctor,它们会因自定义异常而中断
ruslander 2012年

3
如果异常没有序列化构造函数,则建议的解决方案不起作用。我建议使用在stackoverflow.com/a/4557183/209727上提出的解决方案,该解决方案在任何情况下都可以正常工作。对于.NET 4.5,请考虑使用ExceptionDispatchInfo类。
Davide Icardi

33

我认为您最好的选择就是将其放在您的catch块中:

throw;

然后稍后提取内部异常。


21
或完全删除try / catch。
Daniel Earwicker

6
@Earwicker。通常,删除try / catch并不是一个好的解决方案,因为它忽略了在将异常传播到调用堆栈之前需要清理代码的情况。
约旦,

12
@Jordan-清理代码应该在finally块中而不是catch块中
Paolo 2010年

17
@Paolo-如果应该在每种情况下执行,则可以。如果只应在失败的情况下执行,则否。
chiccodoro's

4
请记住,InternalPreserveStackTrace不是线程安全的,因此,如果您在这些异常状态中有两个处于线程状态...愿上帝怜悯我们所有人。
罗布

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

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


请注意,在.Net 4.0中,InternalPreserveStackTrace现在是禁止操作的-在Reflector中查看,您将看到该方法完全为空!
塞缪尔·杰克

从头开始:我正在看RC:在beta中,他们再次将实现放回去!
塞缪尔·杰克2010年

3
建议:更改PreserveStackTrace以返回ex-然后引发异常,您可以说:throw ex.PreserveStackTrace();
Simon_Weaver

为什么要使用Action<Exception>这里使用静态方法
Kiquenet '18

13

没有人解释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将为您提供堆栈跟踪,其中方法的源代码行号CallingMethodthrow调用的行号。这意味着,如果该throw new Exception( "TEST" )行被其他操作包围,则您不知道实际在哪个行号上引发了异常。

情况4与情况2类似,因为保留了原始异常的行号,但不是真正的重新抛出,因为它更改了原始异常的类型。


4
我一直以为'throw'不会重置stacktrace(与'throw e'相对)。
Jesper Matthiesen

@JesperMatthiesen我可能会弄错,但是我听说这取决于是否引发异常并将其捕获在同一文件中。如果是同一文件,则堆栈跟踪将丢失;如果是另一个文件,则将保留堆栈跟踪。
贾胡

10

甚至更多的反思...

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的更多讨论。


28
这是一个非常非常糟糕的主意,因为它取决于有关框架类的内部未记录的详细信息。
Daniel Earwicker

1
事实证明,无需反射即可保留堆栈跟踪,请参见下文。
Anton Tykhyy,2010年

1
调用内部InternalPreserveStackTrace方法会更好,因为它做同样的事情,并且将来不太可能更改...
Thomas Levesque 2010年

1
实际上,情况会更糟,因为Mono上不存在InternalPreserveStackTrace。
Skolima 2010年

5
@daniel-好吧,这是一个非常非常糟糕的主意;在训练每个.net开发人员认为不会的情况时重置stacktrace。如果您找不到NullReferenceException的来源并因为找不到而丢失了客户/订单,这也是一件非常非常非常糟糕的事情。对我来说,胜过“未记录的细节”,而且绝对是单声道。
Simon_Weaver 2010年

10

第一:不要丢失TargetInvocationException-当您要调试东西时,它是有价值的信息。
第二:将TIE作为InnerException包裹在您自己的异常类型中,并放置一个OriginalException属性,该属性链接到您需要的内容(并保持整个调用堆栈不变)。
第三:让TIE冒出您的方法。


5

伙计们,你很酷..我很快就会成为一个死灵法师。

    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
不错的主意,但是您并不总是控制调用的代码.Invoke()
Anton Tykhyy,2010年

1
而且,您也不总是在编译时就知道参数/结果的类型。
罗曼·斯塔科夫

3

使用异常序列化/反序列化的另一个示例代码。它不需要实际的异常类型可序列化。它还仅使用公共/受保护的方法。

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

不需要实际的异常类型可序列化?
Kiquenet '18

3

根据Paul Turners的回答,我提出了一种扩展方法

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

return exIST从未达到,但好处是,我可以使用throw ex.Capture()作为一个衬垫上,这样编译器不会引发not all code paths return a value错误。

    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();
        }
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.