如何在 C# 中重新抛出 InnerException 而不会丢失堆栈跟踪?

Posted

技术标签:

【中文标题】如何在 C# 中重新抛出 InnerException 而不会丢失堆栈跟踪?【英文标题】:How to rethrow InnerException without losing stack trace in C#? 【发布时间】:2010-09-08 14:44:21 【问题描述】:

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

【问题讨论】:

还有另一种不需要任何巫术的方法。看看这里的答案:***.com/questions/15668334/… 动态调用方法抛出的异常是“异常已被调用的目标抛出”异常的内部异常。它有自己的堆栈跟踪。真的没什么好担心的。 使用 mi.Invoke(this, BindingFlags.DoNotWrapExceptions, null, null, null) 【参考方案1】:

.NET 4.5 中现在有 ExceptionDispatchInfo 类。

这让您可以在不更改堆栈跟踪的情况下捕获异常并重新抛出它:

using ExceptionDispatchInfo = 
    System.Runtime.ExceptionServices.ExceptionDispatchInfo;

try

    task.Wait();

catch(AggregateException ex)

    ExceptionDispatchInfo.Capture(ex.InnerException).Throw();

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

它是由于await C# 语言特性而引入的,该特性从AggregateException 实例中解开内部异常,以使异步语言特性更像同步语言特性。

【讨论】:

Exception.Rethrow() 扩展方法的好候选? 请注意,ExceptionDispatchInfo 类位于 System.Runtime.ExceptionServices 命名空间中,在 .NET 4.5 之前不可用。 您可能需要在 .Throw() 行之后放置一个常规的throw;,因为编译器不会知道 .Throw() 总是抛出异常。 throw; 将永远不会被调用,但如果你的方法需要返回对象或者是异步函数,至少编译器不会抱怨。 @Taudris 这个问题专门是关于重新抛出内部异常,throw; 无法特殊处理。如果您使用throw ex.InnerException;,堆栈跟踪将在重新抛出时重新初始化。 @amitjha ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();【参考方案2】:

可以在重新抛出之前保留堆栈跟踪而不进行反射:

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 ;

【讨论】:

看起来很酷,运行这些功能后需要做什么? 实际上,它并不比调用 InternalPreserveStackTrace 慢多少(10000 次迭代大约慢 6%)。通过反射直接访问字段比调用 InternalPreserveStackTrace 快约 2.5% 我建议使用带有字符串或唯一对象键的e.Data 字典(static readonly object myExceptionDataKey = new object (),但如果您必须在任何地方序列化异常,请不要这样做)。避免修改e.Message,因为您可能在某处有解析e.Message 的代码。解析e.Message 是邪恶的,但可能没有其他选择,例如如果您必须使用异常实践不佳的 3rd 方库。 如果自定义异常没有序列化 ctor,DoFixups 会中断 如果异常没有序列化构造函数,则建议的解决方案不起作用。我建议使用***.com/a/4557183/209727 提出的解决方案,在任何情况下都可以很好地工作。对于 .NET 4.5 考虑使用 ExceptionDispatchInfo 类。【参考方案3】:

我认为你最好的办法就是把它放在你的 catch 块中:

throw;

然后再提取内部异常。

【讨论】:

或者完全删除 try/catch。 @Earwicker。删除 try/catch 通常不是一个好的解决方案,因为它忽略了在将异常传播到调用堆栈之前需要清理代码的情况。 @Jordan - 清理代码应该在 finally 块而不是 catch 块中 @Paolo - 如果它应该在每种情况下都执行,是的。如果它应该只在失败的情况下执行,没有。 请记住,InternalPreserveStackTrace 不是线程安全的,所以如果您有 2 个线程处于这些异常状态中...愿上帝怜悯我们所有人。【参考方案4】:

没有人解释过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 类似,因为保留了原始异常的行号,但不是真正的重新抛出,因为它改变了原始异常的类型。

【讨论】:

我一直认为“throw”并没有重置堆栈跟踪(与“throw e”相反)。 @JesperMatthiesen 我可能弄错了,但我听说这取决于是否抛出异常并将其捕获在同一个文件中。如果是同一个文件,堆栈跟踪将丢失,如果是另一个文件,它将被保留。 我在ExceptionDispatchInfo 上可以找到的内容表明,当您想在捕获的上下文之外重新抛出或打开AggregateException 时使用它。我在测试中看到的不同之处在于throw; 无法从堆栈跟踪中的try 中捕获任何行。将示例 3 更改为 try DoThrow(); catch (Exception) throw; ,您会看到 DoThrow 中的 throwing 行 包含在堆栈跟踪中,但仍然不是来自 try 的调用行。而ExceptionDispatchInfo.Capture 包括该行。在主要用例之外似乎仍然需要做太多工作。 示例 3 中的行为被记录为针对 .NET Core 的错误,并在 .NET Core 2.1 中修复:github.com/dotnet/runtime/issues/9518【参考方案5】:
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:在测试版中,他们又把实现放回去了! 建议:更改 PreserveStackTrace 以返回 ex - 然后抛出异常,您可以说: throw ex.PreserveStackTrace(); 为什么使用Action&lt;Exception&gt; ? Here 使用静态方法【参考方案6】:

根据 Paul Turners 的回答,我做了一个扩展方法

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

return ex 从未达到,但优点是我可以将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();
        
    

【讨论】:

【参考方案7】:

更多反思...

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 的进一步讨论。

【讨论】:

这是一个非常非常糟糕的主意,因为它依赖于有关框架类的内部未记录的详细信息。 事实证明可以在没有反射的情况下保留堆栈跟踪,见下文。 调用内部的InternalPreserveStackTrace 方法会更好,因为它做同样的事情并且在未来不太可能改变...... 实际上,情况会更糟,因为 Mono 上不存在 InternalPreserveStackTrace。 @daniel - 这是一个非常非常非常糟糕的投掷主意;当每个 .net 开发人员都被训练相信它不会重置堆栈跟踪时。如果您无法找出 NullReferenceException 的来源并因为找不到客户/订单而失去客户/订单,这也是一件非常非常非常糟糕的事情。对我来说,这胜过“未记录的细节”,而且绝对是单声道。【参考方案8】:

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

【讨论】:

【参考方案9】:

伙计们,你们太酷了。我很快就会成为一名死灵法师。

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

    

【讨论】:

好主意,但你并不总是控制调用.Invoke()的代码。 而且你也不总是知道编译时参数/结果的类型。【参考方案10】:

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

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

【讨论】:

不要求实际的异常类型是可序列化的?

以上是关于如何在 C# 中重新抛出 InnerException 而不会丢失堆栈跟踪?的主要内容,如果未能解决你的问题,请参考以下文章

C# - 线程中止异常(Thread Abort Exception)重新抛出自身

在不丢失堆栈跟踪的情况下重新抛出 Java 中的异常

如何在 lambda 表达式中重新抛出异常? [复制]

如何在 Javascript 中重新抛出异常,但保留堆栈?

Windows.Web.Http.HttpClient 抛出 COMException c#

finally 块下面的代码如何执行?