سؤال

أنا أعيد استثناء مع "رمي" ، لكن stacktrace غير صحيح:

static void Main(string[] args) {
    try {
        try {
            throw new Exception("Test"); //Line 12
        }
        catch (Exception ex) {
            throw; //Line 15
        }
    }
    catch (Exception ex) {
        System.Diagnostics.Debug.Write(ex.ToString());
    }
    Console.ReadKey();
}

يجب أن يكون stacktrace الأيمن:

System.Exception: Test
   at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 12

لكني أحصل على:

System.Exception: Test
   at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 15

لكن السطر 15 هو موضع "رمي". لقد اختبرت هذا مع .NET 3.5.

هل كانت مفيدة؟

المحلول

ربما يكون الرمي مرتين في نفس الطريقة حالة خاصة - لم أتمكن من إنشاء تتبع مكدس حيث تتبع خطوط مختلفة في نفس الطريقة بعضها البعض. كما تقول الكلمة ، يوضح لك "تتبع المكدس" إطارات المكدس التي اجتازها استثناء. وهناك فقط إطار مكدس واحد لكل مكالمة طريقة!

إذا رميت من طريقة أخرى ، throw; لن يزيل الإدخال لـ Foo(), ، كما هو متوقع:

  static void Main(string[] args)
  {
     try
     {
        Rethrower();
     }
     catch (Exception ex)
     {
        Console.Write(ex.ToString());
     }
     Console.ReadKey();
  }

  static void Rethrower()
  {
     try
     {
        Foo();
     }
     catch (Exception ex)
     {
        throw;
     }

  }

  static void Foo()
  {
     throw new Exception("Test"); 
  }

إذا قمت بتعديل Rethrower() واستبدال throw; بواسطة throw ex;, ، ال Foo() الدخول في تتبع المكدس يختفي. مرة أخرى ، هذا هو السلوك المتوقع.

نصائح أخرى

إنه شيء يمكن اعتباره كما هو متوقع. تعديل تتبع المكدس هو حالة معتادة إذا حددت throw ex;, ، سوف FXCOP من إخطارك أن المكدس يتم تعديله. في حال كنت تصنع throw;, ، لا يتم إنشاء أي تحذير ، ولكن لا يزال ، سيتم تعديل التتبع. لسوء الحظ الآن ، من الأفضل عدم التقاط السابقين أو رميه كأحد داخلي. أعتقد أنه ينبغي اعتباره تأثير Windows أو SMTH من هذا القبيل - تحرير. جيف ريختر يصف هذا الموقف بمزيد من التفصيل في "CLR عبر C#":

الرمز التالي يلقي نفس كائن الاستثناء الذي اشتعلت فيه ويتسبب في إعادة تعيين CLR نقطة انطلاقه للاستثناء:

private void SomeMethod() {
  try { ... }
  catch (Exception e) {
    ...
    throw e; // CLR thinks this is where exception originated.
    // FxCop reports this as an error
  }
}

على النقيض من ذلك ، إذا قمت بإعادة تقديم كائن استثناء باستخدام الكلمة الرئيسية الرمي بمفرده ، فإن CLR لا يعيد تعيين نقطة انطلاق المكدس. يعيد الرمز التالي إدخال كائن الاستثناء نفسه الذي تم اكتشافه ، مما تسبب في عدم إعادة تعيين CLR نقطة انطلاقه للاستثناء:

private void SomeMethod() {
  try { ... }
  catch (Exception e) {
    ...
    throw; // This has no effect on where the CLR thinks the exception
    // originated. FxCop does NOT report this as an error
  }
}

في الواقع ، فإن الفرق الوحيد بين شظايا الكود هاتين الكود هو ما يعتقده CLR هو الموقع الأصلي الذي تم فيه طرح الاستثناء. لسوء الحظ ، عند رمي أو إعادة توزيع استثناء ، يقوم Windows بإعادة ضبط نقطة انطلاق المكدس. لذا ، إذا أصبح الاستثناء غير معروف ، فإن موقع المكدس الذي يتم الإبلاغ عنه إلى الإبلاغ عن خطأ Windows هو موقع آخر رمية أو إعادة تربية ، على الرغم من أن CLR يعرف موقع المكدس حيث تم طرح الاستثناء الأصلي. هذا أمر مؤسف لأنه يجعل تطبيقات تصحيح الأخطاء التي فشلت في هذا المجال أكثر صعوبة. لقد وجد بعض المطورين هذا لا يطاق لدرجة أنهم اختاروا طريقة مختلفة لتنفيذ التعليمات البرمجية الخاصة بهم للتأكد من أن تتبع المكدس يعكس حقًا الموقع الذي تم فيه إلقاء استثناء في الأصل:

private void SomeMethod() {
  Boolean trySucceeds = false;
  try {
    ...
    trySucceeds = true;
  }
  finally {
    if (!trySucceeds) { /* catch code goes in here */ }
  }
}

هذا هو القيد المعروف في إصدار Windows من CLR. يستخدم دعم Windows المدمج لمعالجة الاستثناءات (SEH). المشكلة هي ، إنها تعتمد على إطار المكدس والأسلوب يحتوي على إطار مكدس واحد فقط. يمكنك بسهولة حل المشكلة عن طريق تحريك كتلة Try/Catch الداخلية إلى طريقة مساعد آخر ، وبالتالي إنشاء إطار مكدس آخر. والنتيجة الأخرى لهذا القيد هي أن برنامج التحويل البرمجي JIT لن يدخل أي طريقة تحتوي على بيان المحاولة.

كيف يمكنني الحفاظ على stacktrace الحقيقي؟

يمكنك رمي استثناء جديد ، وتضمين الاستثناء الأصلي كاستثناء داخلي.

لكن هذا قبيح ... لفترة أطول ... يجعلك تختار استثناء منصة رمي ....

أنت مخطئ في البشع ولكن الحق في النقطتين الأخريين. قاعدة الإبهام هي: لا تصطاد إلا إذا كنت ستفعل شيئًا معها ، مثل لفه ، أو تعديله ، أو ابتلاعه ، أو تسجيله. إذا قررت ذلك catch وثم throw مرة أخرى ، تأكد من قيامك بشيء ما ، وإلا فقط اتركه يخدع.

قد يتم إغراءك أيضًا بوضع صيد ببساطة حتى تتمكن من الانقطاع داخل الصيد ، ولكن لدى مصحح Visual Studio خيارات كافية لجعل هذه الممارسة غير ضرورية ، حاول استخدام استثناءات الفرصة الأولى أو نقاط التوقف الشرطية بدلاً من ذلك.

تحرير/استبدال

السلوك مختلف في الواقع ، ولكنه راقص. أما بالنسبة لل لماذا السلوك إذا كان مختلفًا ، سأحتاج إلى تأجيل خبير CLR.

تعديل: إجابة أليكسد يبدو أن هذا هو حسب التصميم.

إن إلقاء الاستثناء في نفس الطريقة التي تجذبها يربك الموقف قليلاً ، لذلك دعونا نلقي استثناءً من طريقة أخرى:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Throw();
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    public static void Throw()
    {
        int a = 0;
        int b = 10 / a;
    }
}

إذا throw; يستخدم ، callstack هو (أرقام السطر استبدال رمز):

at Throw():line (int b = 10 / a;)
at Main():line (throw;) // This has been modified

إذا throw ex; يستخدم ، callstack هو:

at Main():line (throw ex;)

إذا لم يتم القبض على الاستثناء ، فإن callstack هو:

at Throw():line (int b = 10 / a;)
at Main():line (Throw())

تم اختباره في .NET 4 / VS 2010

هناك سؤال مكرر هنا.

كما أفهمها - رمي. تم تجميعه في تعليمات MSIL "Rethrow" ويعدل الإطار الأخير من تتبع المكدس.

أتوقع أن يحتفظ بتتبع المكدس الأصلي وإضافة الخط حيث تمت إعادة تراكمه ، ولكن يبدو أنه لا يمكن أن يكون هناك سوى إطار مكدس واحد لكل مكالمة طريقة.

استنتاج: تجنب استخدام رمي. ولف استثناءك في إعادة تربية جديدة - إنه ليس قبيحًا ، إنها أفضل ممارسة.

يمكنك الحفاظ على تتبع المكدس باستخدام

ExceptionDispatchInfo.Capture(ex);

هنا عينة الكود:

    static void CallAndThrow()
    {
        throw new ApplicationException("Test app ex", new Exception("Test inner ex"));
    }

    static void Main(string[] args)
    {
        try
        {
            try
            {
                try
                {
                    CallAndThrow();
                }
                catch (Exception ex)
                {
                    var dispatchException = ExceptionDispatchInfo.Capture(ex);

                    // rollback tran, etc

                    dispatchException.Throw();
                }
            }
            catch (Exception ex)
            {
                var dispatchException = ExceptionDispatchInfo.Capture(ex);

                // other rollbacks

                dispatchException.Throw();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            Console.WriteLine(ex.InnerException.Message);
            Console.WriteLine(ex.StackTrace);
        }

        Console.ReadLine();
    }

سيكون الإخراج شيئًا مثل:

Test app ex
Test inner ex
   at TestApp.Program.CallAndThrow() in D:\Projects\TestApp\TestApp\Program.cs:line 19
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 30
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 38
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 47

حسنًا ، يبدو أن هناك خطأ في إطار .NET ، إذا قمت بإلقاء استثناء ، وأعدت إعادة توحيده في نفس الطريقة ، يتم فقد رقم السطر الأصلي (سيكون السطر الأخير من الطريقة).

Fortunatelly ، رجل ذكي يدعى Fabrice Marguerie حل لهذا الخطأ. أدناه هو الإصدار الخاص بي ، والذي يمكنك اختباره هذا .net كمان.

private static void RethrowExceptionButPreserveStackTrace(Exception exception)
{
    System.Reflection.MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
      System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
    preserveStackTrace.Invoke(exception, null);
      throw exception;
}

الآن استثناء الاستثناء عادة ، ولكن بدلاً من الرمي ؛ ما عليك سوى استدعاء هذه الطريقة ، وفويلا ، سيتم الحفاظ على رقم السطر الأصلي!

لست متأكدًا مما إذا كان هذا حسب التصميم ، لكنني أعتقد أنه كان دائمًا هكذا.

إذا كان الاستثناء الجديد الأصلي الجديد في طريقة منفصلة ، فيجب أن يكون للنتيجة الرمي اسم الطريقة الأصلية ورقم السطر ومن ثم رقم السطر في Main حيث يتم إعادة تراكم الاستثناء.

إذا كنت تستخدم Throw Ex ، فستكون النتيجة هي الخط في Main حيث يتم إعادة الاستثناء.

بمعنى آخر ، يخسر رمي السابق الكل stacktrace ، في حين أن رمي يحافظ على تتبع المكدس التاريخ (أي تفاصيل أساليب المستوى الأدنى). ولكن إذا تم إنشاء استثناءك بنفس الطريقة التي يتم بها إعادة التجديف ، فيمكنك أن تفقد بعض المعلومات.

NB. إذا كتبت برنامج اختبار بسيط وصغير للغاية ، فيمكن أن يقوم الإطار في بعض الأحيان بتحسين الأشياء وتغيير طريقة لتكون رمزًا مضمّنًا مما يعني أن النتائج قد تختلف عن برنامج "حقيقي".

هل تريد رقم السطر الصحيح الخاص بك؟ فقط استخدم واحد حاول/تمسك لكل طريقة. في الأنظمة ، حسناً ... فقط في طبقة واجهة المستخدم ، وليس في المنطق أو الوصول إلى البيانات ، هذا أمر مزعج للغاية ، لأنه إذا كنت بحاجة إلى معاملات قاعدة البيانات ، فلا ينبغي أن تكون في طبقة واجهة المستخدم ، ولن يكون لديك رقم السطر الصحيح ، ولكن إذا لم تكن بحاجة إليهم ، فلا تتم إعادة تجويف معها ولا بدون استثناء في الصيد ...

رمز عينة 5 دقائق:

قائمة الطعام ملف -> مشروع جديد, ، ضع ثلاثة أزرار ، واتصل بالرمز التالي في كل واحد:

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithoutTC();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

private void button2_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithTC1();
    }
    catch (Exception ex)
    {
            MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

private void button3_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithTC2();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

الآن ، قم بإنشاء فصل جديد:

class Class1
{
    public int a;
    public static void testWithoutTC()
    {
        Class1 obj = null;
        obj.a = 1;
    }
    public static void testWithTC1()
    {
        try
        {
            Class1 obj = null;
            obj.a = 1;
        }
        catch
        {
            throw;
        }
    }
    public static void testWithTC2()
    {
        try
        {
            Class1 obj = null;
            obj.a = 1;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}

تشغيل ... الزر الأول جميل!

أعتقد أن هذا أقل حالة من تتبع المكدس وأكثر علاقة بالطريقة التي يتم بها تحديد رقم السطر لتتبع المكدس. تجربته في Visual Studio 2010 ، يشبه السلوك ما تتوقعه من وثائق MSDN: "Throw Ex ؛" يعيد بناء تتبع المكدس من نقطة هذا البيان ، "رمي" ؛ يترك تتبع المكدس كما هو ، باستثناء أنه في أي وقت من الأوقات يتم إعادة الاستثناء ، فإن رقم السطر هو موقع reethrow وليس الاستثناء الذي جاء فيه الاستثناء.

لذلك مع "رمي" ؛ يتم ترك شجرة استدعاء الطريقة دون تغيير ، لكن أرقام الخط قد تتغير.

لقد صادفت هذا عدة مرات ، وقد يكون ذلك حسب التصميم ولم يتم توثيقه بالكامل. أستطيع أن أفهم سبب قيامهم بذلك لأن موقع إعادة التجزئة مفيد للغاية ، وإذا كانت أساليبك بسيطة بما يكفي ، فإن المصدر الأصلي عادة ما يكون واضحًا على أي حال.

كما قال العديد من الأشخاص الآخرين ، من الأفضل عادة عدم الاستثناء ما لم يكن عليك حقًا ذلك ، و/أو ستتعامل معها في تلك المرحلة.

ملاحظة جانبية مثيرة للاهتمام: لن يسمح لي Visual Studio 2010 حتى ببناء الكود كما هو موضح في السؤال لأنه يلتقط الفجوة على خطأ صفر في وقت الترجمة.

هذا لأنك التقطت Exception من الخط 12 وتراجعها السطر 15, ، وبالتالي فإن تتبع المكدس يأخذ نقدًا ، أن Exception تم إلقاؤه من هناك.

للتعامل بشكل أفضل مع الاستثناءات ، يجب عليك ببساطة استخدام try...finally, ، ودع غير ذلك Exception فقاعة تصعد.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top