วิธีการสร้างใหม่ InnerException โดยไม่สูญเสียการติดตามสแต็คใน C # ได้อย่างไร


305

ฉันกำลังเรียกวิธีสะท้อนซึ่งอาจทำให้เกิดข้อยกเว้น ฉันจะส่งข้อยกเว้นไปยังผู้โทรโดยไม่มีการสะท้อนของผู้คลุมรอบได้อย่างไร
ฉันกำลัง rethrowing 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/…
Timothy Shields

ข้อยกเว้นที่เกิดขึ้นในวิธีการที่เรียกแบบไดนามิกคือข้อยกเว้นภายในของข้อยกเว้น "ข้อยกเว้นถูกส่งออกโดยเป้าหมายของการเรียกใช้" ข้อยกเว้น มีการติดตามสแต็กของตัวเอง มีไม่มากที่นั่นจริงๆต้องกังวล
ajeh

คำตอบ:


481

ใน. NET 4.5ขณะนี้มีExceptionDispatchInfoคลาส

สิ่งนี้ช่วยให้คุณสามารถดักจับข้อยกเว้นและทำการโยนใหม่โดยไม่เปลี่ยน stack-trace:

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

นี้ทำงานบนข้อยกเว้นใด ๆ AggregateExceptionที่ไม่เพียง

มันได้รับการแนะนำเนื่องจากawaitคุณสมบัติภาษา C # ซึ่งไม่รวมข้อยกเว้นภายในจากAggregateExceptionอินสแตนซ์เพื่อให้คุณสมบัติทางภาษาแบบอะซิงโครนัสมากกว่าคุณสมบัติทางภาษาแบบซิงโครนัส


11
ผู้สมัครที่ดีสำหรับวิธีการยกเว้น Exth.Rethrow ()
nmarler

8
โปรดสังเกตว่าคลาส ExceptionDispatchInfo อยู่ในเนมสเปซ System.Runtime.ExceptionServices และไม่พร้อมใช้งานก่อนหน้า. NET 4.5
yoyo

53
คุณอาจต้องใส่throw;บรรทัดปกติหลังจากบรรทัด. Throw () เนื่องจากคอมไพเลอร์จะไม่ทราบว่า. Throw () จะส่งข้อยกเว้นเสมอ throw;จะไม่ถูกเรียกเป็นผลลัพธ์ แต่อย่างน้อยคอมไพเลอร์จะไม่บ่นหากวิธีการของคุณต้องการวัตถุส่งคืนหรือเป็นฟังก์ชัน async
ทอดด์

5
@Taudris คำถามนี้เป็นคำถามเฉพาะเกี่ยวกับ rethrowing throw;ยกเว้นภายในซึ่งไม่สามารถจัดการเป็นพิเศษโดย หากคุณใช้throw ex.InnerException;การติดตามสแต็กจะถูกกำหนดค่าเริ่มต้นใหม่ ณ จุดที่มีการโยนทิ้งอีกครั้ง
Paul Turner

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

86

มันเป็นไปได้ที่จะรักษากองติดตามก่อน rethrowing โดยไม่ต้องสะท้อนให้เห็นถึง:

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

2
ที่จริงแล้วมันไม่ช้ากว่าการเรียกใช้InternalPreserveStackTrace(ช้าลงประมาณ 6% เมื่อใช้ซ้ำ 10,000 ครั้ง) การเข้าถึงฟิลด์โดยตรงโดยไตร่ตรองนั้นเร็วกว่าการเรียกใช้ประมาณ 2.5%InternalPreserveStackTrace
Thomas Levesque

1
ฉันขอแนะนำให้ใช้e.Dataพจนานุกรมกับสตริงหรือคีย์วัตถุที่ไม่ซ้ำกัน ( static readonly object myExceptionDataKey = new object ()แต่อย่าทำเช่นนี้หากคุณต้องทำให้เป็นข้อยกเว้นที่ใดก็ได้) หลีกเลี่ยงการปรับเปลี่ยนเพราะคุณอาจจะมีบางรหัสซึ่งจะแยกวิเคราะห์e.Message e.Messageการแยกวิเคราะห์e.Messageเป็นสิ่งที่ชั่วร้าย แต่อาจไม่มีทางเลือกอื่นเช่นหากคุณต้องใช้ห้องสมุดบุคคลที่สามที่มีข้อยกเว้นที่ไม่ดี
Anton Tykhyy

10
แบ่ง DoFixups สำหรับข้อยกเว้นที่กำหนดเองถ้าพวกเขาไม่ได้มี ctor อนุกรม
ruslander

3
โซลูชันที่แนะนำไม่สามารถใช้งานได้หากข้อยกเว้นไม่มีตัวสร้างการทำให้เป็นอนุกรม ฉันแนะนำให้ใช้วิธีแก้ปัญหาที่เสนอไว้ที่stackoverflow.com/a/4557183/209727ซึ่งทำงานได้ดีในทุกกรณี สำหรับ. NET 4.5 ให้พิจารณาใช้คลาส ExceptionDispatchInfo
Davide Icardi

33

ฉันคิดว่าทางออกที่ดีที่สุดของคุณคือเพียงแค่ใส่สิ่งนี้ลงในบล็อก catch ของคุณ:

throw;

แล้วแยกความสว่างของแสงในภายหลัง


21
หรือลบลอง / จับทั้งหมด
Daniel Earwicker

6
@Earwicker การลบ try / catch ไม่ใช่วิธีแก้ปัญหาที่ดีโดยทั่วไปเพราะจะไม่สนใจกรณีที่จำเป็นต้องใช้โค้ดการล้างข้อมูลก่อนที่จะเผยแพร่ข้อยกเว้นในสแต็กการเรียก
จอร์แดน

12
@Jordan - รหัสการทำความสะอาดควรอยู่ในบล็อกในที่สุดไม่ใช่บล็อก catch
Paolo

17
@ เปาโล - ถ้ามันควรจะถูกประหารชีวิตในทุกกรณีใช่ ถ้ามันควรจะถูกดำเนินการเฉพาะในกรณีที่ล้มเหลวไม่ใช่
chiccodoro

4
โปรดทราบว่า InternalPreserveStackTrace ไม่ปลอดภัยสำหรับเธรดดังนั้นหากคุณมี 2 เธรดในสถานะข้อยกเว้นเหล่านี้ ... พระเจ้าอาจทรงเมตตาเราทุกคน
Rob

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: ในรุ่นเบต้าพวกเขานำการติดตั้งกลับมาอีกครั้ง!
ซามูเอลแจ็ค

3
ข้อเสนอแนะ: เปลี่ยน PreserveStackTrace เพื่อส่งคืนอดีต - จากนั้นเพื่อโยนข้อยกเว้นคุณเพียงแค่พูดว่า: โยน ex.PreserveStackTrace ();
Simon_Weaver

ทำไมต้องใช้Action<Exception>? ที่นี่ใช้วิธีการคงที่
Kiquenet

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 จะให้การติดตามสแต็กแก่คุณโดยที่หมายเลขบรรทัดซอร์สโค้ดสำหรับCallingMethodวิธีคือหมายเลขบรรทัดของการthrowโทร ซึ่งหมายความว่าหากthrow new Exception( "TEST" )บรรทัดถูกล้อมรอบด้วยการดำเนินการอื่นคุณไม่มีความคิดว่าหมายเลขบรรทัดใดที่มีข้อยกเว้นเกิดขึ้นจริง

กรณีที่ 4 คล้ายกับ case 2 เนื่องจากหมายเลขบรรทัดของข้อยกเว้นดั้งเดิมถูกเก็บรักษาไว้ แต่ไม่ใช่การสร้างใหม่จริงเนื่องจากจะเปลี่ยนชนิดของข้อยกเว้นดั้งเดิม


4
ฉันคิดเสมอว่า 'การโยน' ไม่ได้ทำการรีเซ็ตสแต็กสแต็ก
Jesper Matthiesen

@JesperMatthiesen ฉันอาจเข้าใจผิด แต่ฉันได้ยินมาว่ามันขึ้นอยู่กับว่ามีการโยนข้อยกเว้นและติดอยู่ในไฟล์เดียวกัน หากเป็นไฟล์เดียวกันการติดตามสแต็กจะหายไปหากเป็นไฟล์อื่นไฟล์นั้นจะถูกเก็บรักษาไว้
jahu

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 ดูการอภิปรายเพิ่มเติมเกี่ยวกับBugzilla โมโน


28
นี่เป็นความคิดที่แย่จริงๆเพราะมันขึ้นอยู่กับรายละเอียดที่ไม่มีเอกสารเกี่ยวกับคลาสของกรอบงาน
Daniel Earwicker

1
ปรากฎว่ามีความเป็นไปได้ที่จะรักษาสแต็กติดตามโดยไม่มีการสะท้อนดูด้านล่าง
Anton Tykhyy

1
การเรียกใช้InternalPreserveStackTraceวิธีการภายในจะดีกว่าเนื่องจากมันทำสิ่งเดียวกันและมีโอกาสน้อยที่จะเปลี่ยนแปลงในอนาคต ...
Thomas Levesque

1
ที่จริงแล้วมันจะแย่กว่านี้เพราะ InternalPreserveStackTrace ไม่มีอยู่ใน Mono
skolima

5
@Daniel - มันเป็นความคิดที่ดีจริงๆสำหรับการโยน; เพื่อรีเซ็ต stacktrace เมื่อผู้พัฒนา. net ทุกคนได้รับการฝึกฝนให้เชื่อว่าจะไม่เกิดขึ้น มันเป็นสิ่งที่เลวร้ายจริงๆถ้าคุณไม่สามารถหาแหล่งที่มาของ NullReferenceException และสูญเสียลูกค้า / คำสั่งซื้อเพราะคุณไม่พบมัน สำหรับฉันแล้วสิ่งที่สำคัญกว่า 'รายละเอียดที่ไม่มีเอกสาร' และแน่นอนที่สุด
Simon_Weaver

10

ข้อแรก: อย่าเสีย TargetInvocationException - เป็นข้อมูลที่มีค่าเมื่อคุณต้องการแก้ไขข้อบกพร่อง
ข้อสอง: ตัด TIE เป็น InnerException ในประเภทยกเว้นของคุณและใส่คุณสมบัติ OriginalException ที่เชื่อมโยงกับสิ่งที่คุณต้องการ (และทำให้ callstack ทั้งหมดยังคงอยู่)
ประการที่สาม: ปล่อยให้ฟอง 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

1
และคุณไม่เคยรู้ชนิดของอาร์กิวเมนต์ / ผลลัพธ์ในเวลารวบรวมเช่นกัน
Roman Starkov

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

3

จากคำตอบของพอลเทอร์เนอร์ฉันทำวิธีการขยาย

    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();
        }
    }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.