Bagaimana cara rethrow InnerException tanpa kehilangan jejak stack di C #?

305

Saya memanggil, melalui refleksi, metode yang dapat menyebabkan pengecualian. Bagaimana saya bisa meneruskan pengecualian ke pemanggil saya tanpa refleksi pembungkus meletakkan di sekitarnya?
Saya memikirkan kembali InnerException, tetapi ini menghancurkan jejak stack.
Kode contoh:

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;
    }
}
skolima
sumber
1
Ada cara lain untuk melakukan ini yang tidak memerlukan voodoo. Lihatlah jawabannya di sini: stackoverflow.com/questions/15668334/…
Timothy Shields
Pengecualian yang dilemparkan dalam metode yang disebut secara dinamis adalah pengecualian dalam dari pengecualian "Pengecualian telah dilemparkan oleh target doa". Ini memiliki jejak tumpukannya sendiri. Tidak ada yang perlu dikhawatirkan.
ajeh

Jawaban:

481

Di .NET 4.5 sekarang ada ExceptionDispatchInfokelasnya.

Ini memungkinkan Anda menangkap pengecualian dan melemparkannya kembali tanpa mengubah tumpukan-jejak:

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

Ini bekerja dengan pengecualian apa pun, bukan hanya AggregateException.

Itu diperkenalkan karena awaitfitur bahasa C #, yang membuka bungkus pengecualian dalam AggregateExceptioninstance untuk membuat fitur bahasa asinkron lebih seperti fitur bahasa sinkron.

Paul Turner
sumber
11
Kandidat yang bagus untuk metode ekstensi Exception.Rethrow ()?
nmarler
8
Perhatikan bahwa kelas ExceptionDispatchInfo berada di namespace System.Runtime.ExceptionServices, dan tidak tersedia sebelum .NET 4.5.
yoyo
53
Anda mungkin perlu meletakkan regular throw;setelah baris .Throw (), karena kompiler tidak akan tahu itu .Throw () selalu melempar pengecualian. throw;tidak akan pernah dipanggil sebagai hasilnya, tetapi setidaknya kompiler tidak akan mengeluh jika metode Anda memerlukan objek kembali atau fungsi async.
Todd
5
@Taudris Pertanyaan ini khusus tentang memikirkan kembali pengecualian dalam, yang tidak dapat ditangani secara khusus oleh throw;. Jika Anda menggunakan throw ex.InnerException;stack-trace diinisialisasi ulang pada titik itu dilemparkan kembali.
Paul Turner
5
@amitjhaExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
Vedran
86

Hal ini dimungkinkan untuk melestarikan jejak stack sebelum rethrowing tanpa refleksi:

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
}

Ini menghabiskan banyak siklus dibandingkan dengan menelepon InternalPreserveStackTracemelalui delegasi yang di-cache, tetapi memiliki keuntungan hanya mengandalkan fungsi publik. Berikut adalah beberapa pola penggunaan umum untuk fungsi pelestarian tumpukan-jejak:

// 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 ;
}
Anton Tykhyy
sumber
Terlihat keren, apa yang perlu terjadi setelah menjalankan fungsi-fungsi ini?
vdboor
2
Sebenarnya, itu tidak jauh lebih lambat daripada memohon InternalPreserveStackTrace(sekitar 6% lebih lambat dengan 10.000 iterasi). Mengakses ladang secara langsung dengan refleksi adalah sekitar 2,5% lebih cepat daripada memohonInternalPreserveStackTrace
Thomas Levesque
1
Saya akan merekomendasikan menggunakan e.Datakamus dengan string atau kunci objek unik ( static readonly object myExceptionDataKey = new object (), tapi jangan lakukan ini jika Anda harus membuat serialkan pengecualian di mana saja). Hindari memodifikasi e.Message, karena Anda mungkin memiliki kode di suatu tempat yang mem-parsing e.Message. Parsing e.Messageitu jahat, tetapi mungkin tidak ada pilihan lain, misalnya jika Anda harus menggunakan perpustakaan pihak ketiga dengan praktik pengecualian yang buruk.
Anton Tykhyy
10
Istirahat DoFixups untuk pengecualian khusus jika mereka tidak memiliki ctor serialisasi
ruslander
3
Solusi yang disarankan tidak berfungsi jika pengecualian tidak memiliki konstruktor serialisasi. Saya menyarankan untuk menggunakan solusi yang diusulkan di stackoverflow.com/a/4557183/209727 yang berfungsi dengan baik dalam hal apa pun. Untuk .NET 4.5 pertimbangkan untuk menggunakan kelas ExceptionDispatchInfo.
Davide Icardi
33

Saya pikir taruhan terbaik Anda adalah dengan meletakkan ini di blok tangkapan Anda:

throw;

Dan kemudian ekstrak innerexception nanti.

GEOCHET
sumber
21
Atau hapus mencoba / menangkap sama sekali.
Daniel Earwicker
6
@Earwicker. Menghapus try / catch bukan solusi yang baik secara umum karena mengabaikan kasus-kasus di mana kode pembersihan diperlukan sebelum menyebarkan pengecualian ke tumpukan panggilan.
Jordan
12
@Jordan - Bersihkan kode harus dalam blok akhirnya bukan blok tangkap
Paolo
17
@ Paolo - Jika seharusnya dieksekusi dalam setiap kasus, ya. Jika seharusnya dieksekusi hanya dalam kasus kegagalan, tidak.
chiccodoro
4
Perlu diingat bahwa InternalPreserveStackTrace bukan utas yang aman, jadi jika Anda memiliki 2 utas dalam salah satu dari status pengecualian ini ... semoga tuhan kasihanilah kami semua.
Rob
14

Tidak ada yang menjelaskan perbedaan antara ExceptionDispatchInfo.Capture( ex ).Throw()dan dataran throw, jadi ini dia.

Cara lengkap untuk mengubah kembali pengecualian yang tertangkap adalah menggunakan ExceptionDispatchInfo.Capture( ex ).Throw()(hanya tersedia dari .Net 4.5).

Di bawah ini ada kasus yang diperlukan untuk menguji ini:

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

Kasus 1 dan kasus 2 akan memberi Anda jejak tumpukan di mana nomor baris kode sumber untuk CallingMethodmetode ini adalah nomor baris dari throw new Exception( "TEST" )baris tersebut.

Namun, kasus 3 akan memberi Anda jejak tumpukan di mana nomor baris kode sumber untuk CallingMethodmetode ini adalah nomor baris throwpanggilan. Ini berarti bahwa jika throw new Exception( "TEST" )garis dikelilingi oleh operasi lain, Anda tidak tahu di mana nomor garis pengecualian itu sebenarnya dilemparkan.

Kasus 4 mirip dengan kasus 2 karena nomor baris pengecualian asli dipertahankan, tetapi bukan rethrow nyata karena mengubah jenis pengecualian asli.

jeuoekdcwzfwccu
sumber
5
Saya selalu berpikir bahwa 'throw' tidak mereset stacktrace (sebagai lawan dari 'throw e').
Jesper Matthiesen
@JesperMatthiesen Saya mungkin salah, tetapi saya mendengar bahwa itu tergantung jika pengecualian dilemparkan dan ditangkap dalam file yang sama. Jika itu file yang sama, jejak stack akan hilang, jika itu file lain, itu akan dipertahankan.
1919
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 );
    }
}

Panggil metode ekstensi pada pengecualian Anda sebelum Anda melemparkannya, itu akan mempertahankan jejak tumpukan asli.

Eric
sumber
Ketahuilah bahwa di .Net 4.0, InternalPreserveStackTrace sekarang menjadi tanpa-pilihan di Reflector dan Anda akan melihat metode ini benar-benar kosong!
Samuel Jack
Gores itu: Saya melihat RC: dalam versi beta, mereka telah mengembalikan implementasinya!
Samuel Jack
3
saran: ubah PreserveStackTrace untuk mengembalikan ex - lalu untuk melempar pengecualian, Anda bisa mengatakan: throw ex.PreserveStackTrace ();
Simon_Weaver
Mengapa menggunakan Action<Exception>? Di sini menggunakan metode statis
Kiquenet
10

Bahkan lebih banyak refleksi ...

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

Ingatlah bahwa ini dapat pecah kapan saja, karena bidang pribadi bukan bagian dari API. Lihat diskusi lebih lanjut tentang Mono bugzilla .

skolima
sumber
28
Ini adalah ide yang sangat, sangat buruk, karena ini tergantung pada detail tidak berdokumen internal tentang kelas kerangka kerja.
Daniel Earwicker
1
Ternyata mungkin untuk mempertahankan jejak tumpukan tanpa Refleksi, lihat di bawah.
Anton Tykhyy
1
Memanggil InternalPreserveStackTracemetode internal akan lebih baik, karena melakukan hal yang sama dan kecil kemungkinannya berubah di masa depan ...
Thomas Levesque
1
Sebenarnya, itu akan menjadi lebih buruk, karena InternalPreserveStackTrace tidak ada di Mono.
skolima
5
@aniel - well itu ide yang sangat, sangat, sangat buruk untuk dilempar; untuk me-reset stacktrace ketika setiap pengembang .net dilatih untuk percaya itu tidak akan. itu juga hal yang sangat, sangat, sangat buruk jika Anda tidak dapat menemukan sumber NullReferenceException dan kehilangan pelanggan / pesanan karena Anda tidak dapat menemukannya. bagi saya yang mengalahkan 'detail tidak berdokumen' dan pasti mono.
Simon_Weaver
10

Pertama: jangan kehilangan TargetInvocationException - ini informasi berharga ketika Anda ingin men-debug sesuatu.
Kedua: Bungkus TIE sebagai InnerException dalam tipe pengecualian Anda sendiri dan letakkan properti OriginalException yang menautkan ke apa yang Anda butuhkan (dan pertahankan seluruh panggilan tetap utuh).
Ketiga: Biarkan gelembung TIE keluar dari metode Anda.

kokos
sumber
5

Kawan, kau keren .. Aku akan segera menjadi ahli nujum.

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

    }
Boris Treukhov
sumber
1
Ide bagus, tetapi Anda tidak selalu mengontrol kode panggilan .Invoke().
Anton Tykhyy
1
Dan Anda tidak selalu tahu jenis argumen / hasil pada waktu kompilasi juga.
Roman Starkov
3

Kode contoh lain yang menggunakan serialisasi / deserialisasi pengecualian. Itu tidak memerlukan jenis pengecualian aktual menjadi serializable. Juga hanya menggunakan metode publik / dilindungi.

    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 });
    }
produk sampingan ayam
sumber
tidak memerlukan jenis pengecualian aktual untuk dapat serial?
Kiquenet
3

Berdasarkan jawaban Paul Turners saya membuat metode ekstensi

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

yang return exist tidak pernah mencapai tapi keuntungan adalah bahwa saya dapat menggunakan throw ex.Capture()sebagai salah satu kapal sehingga compiler tidak akan menaikkan not all code paths return a valuekesalahan.

    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();
        }
    }
Jürgen Steinblock
sumber