簡體   English   中英

如何在 C# 中重新拋出 InnerException 而不會丟失堆棧跟蹤?

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

我正在通過反射調用一個可能導致異常的方法。 如何在沒有包裝反射的情況下將異常傳遞給調用者?
我正在重新拋出 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;
    }
}

.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實例解開內部異常,以使異步語言功能更像同步語言功能。

可能的,而不反射重新拋出之前保存堆棧跟蹤:

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

我認為你最好的選擇是把它放在你的 catch 塊中:

throw;

然后稍后提取內部異常。

沒有人解釋過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 類似,因為保留了原始異常的行號,但不是真正的重新拋出,因為它更改了原始異常的類型。

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

在拋出異常之前調用擴展方法,它將保留原始堆棧跟蹤。

根據 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();
        }
    }

更多的反思...

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 的進一步討論。

第一:不要丟失 TargetInvocationException - 當您想要調試事物時,它是有價值的信息。
第二:將 TIE 作為 InnerException 包裝在您自己的異常類型中,並放置一個鏈接到您需要的內容的 OriginalException 屬性(並保持整個調用堆棧完好無損)。
第三:讓 TIE 從你的方法中冒出來。

伙計們,你們很酷……我很快就會成為一名死靈法師。

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

    }

另一個使用異常序列化/反序列化的示例代碼。 它不需要實際的異常類型是可序列化的。 它也只使用公共/受保護的方法。

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

這只是這里一些其他想法的干凈、現代的實現,在 .NET 6 中進行了測試:

public static class ExceptionExtensions
{
    [DoesNotReturn]
    public static void Rethrow(this Exception ex) 
        => ExceptionDispatchInfo.Capture(ex).Throw();
}

我想要myObject上的PropertyName屬性的值,但這在使用反射調用方法(根據 OP 的問題)或任何其他導致您想要重新拋出內部異常的情況下同樣有效。

try
{
    object? value = myObject.GetType().GetProperty("PropertyName")?.GetValue(myObject);
}
catch (TargetInvocationException ex)
{
    (ex.InnerException ?? ex).Rethrow();
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM