Làm cách nào để lấy lại InternalException mà không mất dấu vết ngăn xếp trong C #?


305

Tôi đang gọi, thông qua sự phản ánh, một phương pháp có thể gây ra một ngoại lệ. Làm thế nào tôi có thể chuyển ngoại lệ cho người gọi của mình mà không có sự phản chiếu của trình bao bọc xung quanh nó?
Tôi đang điều chỉnh lại InternalException, nhưng điều này sẽ phá hủy dấu vết ngăn xếp.
Mã ví dụ:

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
Có một cách khác để làm điều này mà không yêu cầu bất kỳ voodoo. Hãy xem câu trả lời tại đây: stackoverflow.com/questions/15668334/ khăn
Timothy Shields

Ngoại lệ được ném trong phương thức được gọi động là ngoại lệ bên trong của ngoại lệ "Ngoại lệ đã bị ném bởi mục tiêu của lệnh gọi". Nó có dấu vết ngăn xếp riêng của mình. Thực sự không có nhiều thứ khác để lo lắng.
ajeh

Câu trả lời:


481

Trong .NET 4.5 hiện cóExceptionDispatchInfo lớp.

Điều này cho phép bạn nắm bắt một ngoại lệ và ném lại mà không thay đổi dấu vết ngăn xếp:

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

Điều này hoạt động trên bất kỳ ngoại lệ, không chỉ AggregateException .

Nó được giới thiệu do awaittính năng ngôn ngữ C #, giúp loại bỏ các ngoại lệ bên trong khỏi các AggregateExceptionphiên bản để làm cho các tính năng ngôn ngữ không đồng bộ giống với các tính năng ngôn ngữ đồng bộ hơn.


11
Ứng cử viên tốt cho phương pháp mở rộng Exception.Rethrow ()?
nmarler

8
Lưu ý rằng lớp ExceptionDispatchInfo nằm trong không gian tên System.R.78.ExceptionService và không có sẵn trước .NET 4.5.
yoyo

53
Bạn có thể cần đặt một dòng thông thường throw;sau dòng .Throw (), vì trình biên dịch sẽ không biết rằng .Throw () luôn ném ra một ngoại lệ. throw;sẽ không bao giờ được gọi là kết quả, nhưng ít nhất trình biên dịch sẽ không phàn nàn nếu phương thức của bạn yêu cầu một đối tượng trả về hoặc là một hàm không đồng bộ.
Todd

5
@Taudris Câu hỏi này đặc biệt về việc lấy lại ngoại lệ bên trong, không thể được xử lý đặc biệt bởi throw;. Nếu bạn sử dụng throw ex.InnerException;theo dõi ngăn xếp được khởi tạo lại tại điểm nó được ném lại.
Paul Turner

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

86

Đó khả năng để bảo vệ vết đống trước rethrowing mà không cần suy nghĩ:

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
}

Điều này lãng phí rất nhiều chu kỳ so với việc gọi InternalPreserveStackTracequa đại biểu được lưu trong bộ nhớ cache, nhưng có lợi thế là chỉ dựa vào chức năng công cộng. Dưới đây là một vài mẫu sử dụng phổ biến cho các chức năng bảo quản theo dõi ngăn xếp:

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

Trông thật tuyệt, những gì cần phải xảy ra sau khi chạy các chức năng này?
vdboor

2
Trên thực tế, nó không chậm hơn nhiều so với việc gọi InternalPreserveStackTrace( chậm hơn khoảng 6% với 10000 lần lặp). Truy cập trực tiếp vào các trường bằng phản xạ nhanh hơn khoảng 2,5% so với việc gọiInternalPreserveStackTrace
Thomas Levesque

1
Tôi khuyên bạn nên sử dụng e.Datatừ điển với một chuỗi hoặc một khóa đối tượng duy nhất ( static readonly object myExceptionDataKey = new object ()nhưng không làm điều này nếu bạn phải tuần tự hóa các ngoại lệ ở bất cứ đâu). Tránh sửa đổi e.Message, bởi vì bạn có thể có mã ở đâu đó phân tích cú pháp e.Message. Phân tích cú pháp e.Messagelà xấu, nhưng có thể không có lựa chọn nào khác, ví dụ nếu bạn phải sử dụng thư viện của bên thứ 3 với các thực tiễn ngoại lệ kém.
Anton Tykhyy

10
DoFixups phá vỡ các trường hợp ngoại lệ tùy chỉnh nếu chúng không có ctor tuần tự hóa
ruslander

3
Giải pháp được đề xuất không hoạt động nếu ngoại lệ không có hàm tạo tuần tự hóa. Tôi đề nghị sử dụng giải pháp được đề xuất tại stackoverflow.com/a/4557183/209727 hoạt động tốt trong mọi trường hợp. Đối với .NET 4.5, hãy xem xét sử dụng lớp ExceptionDispatchInfo.
Davide Icardi

33

Tôi nghĩ rằng đặt cược tốt nhất của bạn sẽ là chỉ đặt điều này trong khối bắt của bạn:

throw;

Và sau đó trích xuất nhận thức innerexception sau.


21
Hoặc loại bỏ thử / bắt hoàn toàn.
Daniel Earwicker

6
@Earwicker. Nói chung, loại bỏ thử / bắt không phải là một giải pháp tốt vì nó bỏ qua các trường hợp yêu cầu mã dọn dẹp trước khi truyền ngoại lệ lên ngăn xếp cuộc gọi.
Jordan

12
@Jordan - Làm sạch mã phải ở trong một khối cuối cùng không phải là một khối bắt
Paolo

17
@Paolo - Nếu nó được cho là được thực hiện trong mọi trường hợp, vâng. Nếu nó được cho là chỉ được thực hiện trong trường hợp thất bại, thì không.
chiccodoro

4
Hãy nhớ rằng InternalPreserveStackTrace không phải là chủ đề an toàn, vì vậy nếu bạn có 2 luồng trong các trạng thái ngoại lệ này ... có thể chúa sẽ thương xót tất cả chúng ta.
Cướp

14

Không ai giải thích được sự khác biệt giữa ExceptionDispatchInfo.Capture( ex ).Throw()và đồng bằng throw, vì vậy đây là.

Cách hoàn chỉnh để lấy lại một ngoại lệ bị bắt là sử dụng ExceptionDispatchInfo.Capture( ex ).Throw()(chỉ có sẵn từ .Net 4.5).

Dưới đây là các trường hợp cần thiết để kiểm tra điều này:

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

Trường hợp 1 và trường hợp 2 sẽ cung cấp cho bạn một dấu vết ngăn xếp trong đó số dòng mã nguồn cho CallingMethodphương thức là số dòng củathrow new Exception( "TEST" ) dòng.

Tuy nhiên, trường hợp 3 sẽ cung cấp cho bạn một dấu vết ngăn xếp trong đó số dòng mã nguồn cho CallingMethodphương thức là số dòng của throwcuộc gọi. Điều này có nghĩa là nếu throw new Exception( "TEST" )dòng được bao quanh bởi các hoạt động khác, bạn không biết số dòng nào thực sự bị ném.

Trường hợp 4 tương tự với trường hợp 2 vì số dòng của ngoại lệ ban đầu được giữ nguyên, nhưng không phải là một suy nghĩ thực sự vì nó thay đổi loại ngoại lệ ban đầu.


5
Tôi luôn nghĩ rằng 'throw' đã không thiết lập lại stacktrace (trái ngược với 'throw e').
Jesper Matthiesen

@JesperMatthiesen Tôi có thể bị nhầm, nhưng tôi nghe nói rằng điều đó phụ thuộc vào việc ngoại lệ có bị ném và bị bắt trong cùng một tệp không. Nếu là cùng một tệp, dấu vết ngăn xếp sẽ bị mất, nếu đó là một tệp khác, nó sẽ được giữ nguyên.
jahu

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

Gọi phương thức mở rộng về ngoại lệ của bạn trước khi bạn ném nó, nó sẽ giữ nguyên dấu vết ngăn xếp ban đầu.


Xin lưu ý rằng trong .Net 4.0, InternalPreserveStackTrace hiện không phải là một công cụ tìm kiếm trong Reflector và bạn sẽ thấy phương thức này hoàn toàn trống rỗng!
Samuel Jack

Cào rằng: Tôi đã xem RC: trong bản beta, họ đã đưa việc thực hiện trở lại!
Samuel Jack

3
đề xuất: thay đổi PreserveStackTrace để trả về ex - sau đó để ném một ngoại lệ bạn chỉ có thể nói: throw ex.PreserveStackTrace ();
Simon_Weaver

Tại sao nên sử dụng Action<Exception>? Ở đây sử dụng phương pháp tĩnh
Kiquenet

10

Thậm chí nhiều phản ánh ...

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

Hãy nhớ rằng điều này có thể bị hỏng bất cứ lúc nào, vì các trường riêng không phải là một phần của API. Xem thảo luận thêm về Mono bugzilla .


28
Đây là một ý tưởng thực sự, thực sự tồi tệ, vì nó phụ thuộc vào các chi tiết không có giấy tờ nội bộ về các lớp khung.
Daniel Earwicker

1
Hóa ra có thể lưu giữ dấu vết ngăn xếp mà không cần Reflection, xem bên dưới.
Anton Tykhyy

1
Gọi InternalPreserveStackTracephương thức nội bộ sẽ tốt hơn, vì nó cũng làm điều tương tự và ít có khả năng thay đổi trong tương lai ...
Thomas Levesque

1
Trên thực tế, nó sẽ tồi tệ hơn, vì InternalPreserveStackTrace không tồn tại trên Mono.
skolima

5
@daniel - đó là một ý tưởng thực sự, thực sự, thực sự tồi tệ cho ném; để đặt lại stacktrace khi mọi nhà phát triển .net được đào tạo để tin rằng nó sẽ không. nó cũng là một điều thực sự, thực sự, thực sự tồi tệ nếu bạn không thể tìm ra nguồn gốc của NullReferenceException và mất một khách hàng / đơn hàng vì bạn không thể tìm thấy nó. đối với tôi, đó là những "chi tiết không có giấy tờ" và chắc chắn là đơn sắc.
Simon_Weaver

10

Đầu tiên: không mất TargetInvocationException - đó là thông tin có giá trị khi bạn muốn gỡ lỗi mọi thứ.
Thứ hai: Bao bọc TIE dưới dạng InternalException trong loại ngoại lệ của riêng bạn và đặt thuộc tính OriginalException liên kết đến những gì bạn cần (và giữ nguyên toàn bộ bảng gọi).
Thứ ba: Hãy để bong bóng TIE ra khỏi phương pháp của bạn.


5

Các bạn, bạn thật tuyệt .. Tôi sẽ sớm trở thành một thầy chiêu hồn.

    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
Ý tưởng hay, nhưng bạn không phải lúc nào cũng kiểm soát mã cuộc gọi .Invoke().
Anton Tykhyy

1
Và bạn không phải lúc nào cũng biết các loại đối số / kết quả tại thời điểm biên dịch.
Roman Starkov

3

Mã mẫu anpother sử dụng tuần tự hóa / giải tuần tự ngoại lệ. Nó không yêu cầu loại ngoại lệ thực tế để được tuần tự hóa. Ngoài ra, nó chỉ sử dụng phương pháp công cộng / được bảo vệ.

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

không yêu cầu loại ngoại lệ thực tế để được tuần tự hóa?
Kiquenet

3

Dựa trên câu trả lời của Paul Turners tôi đã thực hiện một phương pháp mở rộng

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

các return exist bao giờ đạt được nhưng lợi thế là tôi có thể sử dụng throw ex.Capture()như một lót để trình biên dịch sẽ không tăng not all code paths return a valuelỗi.

    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();
        }
    }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.