لماذا تعتبر تجربة {…} أخيرًا {…} جيدة؛حاول {…} التقاط{} سيء؟

StackOverflow https://stackoverflow.com/questions/128818

سؤال

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

StreamReader reader=new  StreamReader("myfile.txt");
try
{
  int i = 5 / 0;
}
catch   // No args, so it will catch any exception
{}
reader.Close();

ومع ذلك، يعتبر هذا شكلاً جيدًا:

StreamReader reader=new  StreamReader("myfile.txt");
try
{
  int i = 5 / 0;
}
finally   // Will execute despite any exception
{
  reader.Close();
}

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

خلاف ذلك، ما هو الشيء المميز في النهاية؟

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

المحلول

الفرق الكبير هو ذلك try...catch سوف يبتلع الاستثناء، ويخفي حقيقة حدوث خطأ. try..finally سيتم تشغيل رمز التنظيف الخاص بك وبعد ذلك سيستمر الاستثناء، ليتم التعامل معه بواسطة شيء يعرف ما يجب فعله به.

نصائح أخرى

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

"Catch" عبارة عن عبارة "يمكنني التعافي من هذا الاستثناء".يجب عليك فقط التعافي من الاستثناءات التي يمكنك تصحيحها حقًا - يقول الالتقاط بدون وسيطات "مرحبًا، يمكنني التعافي من أي شيء!"، وهو ما يكون دائمًا غير صحيح.

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

لأنه عندما يطرح هذا السطر استثناءً، فلن تعرفه.

مع الكتلة الأولى من التعليمات البرمجية، سيكون الاستثناء ببساطة يمتص, ، سيستمر البرنامج في التنفيذ حتى عندما تكون حالة البرنامج خاطئة.

مع الكتلة الثانية، سيكون الاستثناء ألقيت وفقاعات تصل لكن ال reader.Close() لا يزال مضمونا للتشغيل.

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

وأخيرا يتم تنفيذه مهما حدث.لذلك، إذا نجحت كتلة المحاولة، فسيتم تنفيذها، وإذا فشلت كتلة المحاولة، فسيتم تنفيذ كتلة الالتقاط، ثم الكتلة النهائية.

ومن الأفضل أيضًا محاولة استخدام البنية التالية:

using (StreamReader reader=new  StreamReader("myfile.txt"))
{
}

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

في حين أن كتلتي التعليمات البرمجية التاليتين متكافئتين، إلا أنهما غير متساويتين.

try
{
  int i = 1/0; 
}
catch
{
  reader.Close();
  throw;
}

try
{
  int i = 1/0;
}
finally
{
  reader.Close();
}
  1. "أخيرًا" هو رمز يكشف النية.أنت تعلن للمترجم والمبرمجين الآخرين أن هذا الكود يحتاج إلى التشغيل مهما حدث.
  2. إذا كان لديك كتل التقاط متعددة ولديك رمز تنظيف، فأنت بحاجة أخيرًا.وبدون ذلك، ستقوم بتكرار كود التنظيف الخاص بك في كل كتلة التقاط.(مبدأ الجفاف)

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

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

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

using (StreamReader reader = new StreamReader('myfile.txt'))
{
    // do stuff here
} // reader.dispose() is called automatically

يمكنك استخدام عبارة "استخدام" مع أي كائن يقوم بتنفيذ IDisposable.يتم استدعاء أسلوب التخلص () الخاص بالكائن تلقائيًا في نهاية الكتلة.

ستستمر كتلة المحاولة..الأخيرة في طرح أي استثناءات يتم طرحها.الجميع finally ما يفعله هو التأكد من تشغيل كود التنظيف قبل طرح الاستثناء.

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

سبب آخر هو أن محاولة الالتقاط التي تم إجراؤها بهذه الطريقة غير صحيحة تمامًا.ما تقوله من خلال القيام بذلك هو ، "بغض النظر عما يحدث ، يمكنني التعامل معه". ماذا عن StackOverflowException, هل يمكنك التنظيف بعد ذلك؟ماذا عن OutOfMemoryException؟بشكل عام، يجب عليك فقط التعامل مع الاستثناءات التي تتوقعها وتعرف كيفية التعامل معها.

يستخدم Try..Catch..Finally, ، إذا كانت طريقتك تعرف كيفية التعامل مع الاستثناء محليًا.يحدث الاستثناء في المحاولة، ويتم التعامل معه في Catch وبعد ذلك تتم عملية التنظيف في أخيرًا.

في حالة ما إذا كانت طريقتك لا تعرف كيفية التعامل مع الاستثناء ولكنها تحتاج إلى التنظيف بمجرد حدوثه، استخدمه Try..Finally

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

بواسطة Try..Finally يتم التأكد من إجراء التنظيف المحلي قبل نشر الاستثناء إلى أساليب الاستدعاء.

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

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

من منظور قابلية القراءة ، فإنه يخبر بشكل أكثر صراحة من قارئ الكود المستقبلي "هذه الأشياء هنا مهمة ، يجب القيام بها بغض النظر عما يحدث." هذا جيد.

كما أن عبارات الالتقاط الفارغة تميل إلى أن يكون لها "رائحة" معينة بالنسبة لها.قد تكون علامة على أن المطورين لا يفكرون في الاستثناءات المختلفة التي يمكن أن تحدث وكيفية التعامل معها.

"أخيرًا" اختياري - ليس هناك سبب لوجود كتلة "أخيرًا" إذا لم تكن هناك موارد للتنظيف.

مأخوذ من: هنا

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

DIM STR AS DTREAM = GETTREAM () إذا (str.canread) ثم رمز "لقراءة دفق نهاية إذا

يعتمد قرار التحقق من حالة الكائن قبل استدعاء طريقة معينة قد تثير استثناءً على الحالة المتوقعة للكائن.إذا تم إنشاء كائن FileStream باستخدام مسار ملف يجب أن يكون موجودًا ومنشئًا يجب أن يُرجع ملفًا في وضع القراءة، فإن التحقق من خاصية CanRead ليس ضروريًا؛قد يكون عدم القدرة على قراءة FileStream بمثابة انتهاك للسلوك المتوقع لاستدعاءات الطريقة التي تم إجراؤها، ويجب رفع الاستثناء.في المقابل، إذا تم توثيق أسلوب ما كإرجاع مرجع FileStream الذي قد يكون أو لا يكون قابلاً للقراءة، فمن المستحسن التحقق من خاصية CanRead قبل محاولة قراءة البيانات.

لتوضيح تأثير الأداء الذي يمكن أن يسببه استخدام تقنية الترميز "التشغيل حتى الاستثناء"، تتم مقارنة أداء الإرسال، الذي يطرح InvalidCastException في حالة فشل الإرسال، مع C# كمشغل، والذي يُرجع القيم الخالية في حالة فشل الإرسال.إن أداء الطريقتين متطابق في الحالة التي تكون فيها الجبيرة صالحة (انظر الاختبار 8.05)، ولكن بالنسبة للحالة التي تكون فيها الجبيرة غير صالحة، ويؤدي استخدام الجبيرة إلى حدوث استثناء، فإن استخدام الجبيرة أبطأ بمقدار 600 مرة من استخدام الجبيرة. كمشغل (انظر الاختبار 8.06).يشمل التأثير عالي الأداء لتقنية رمي الاستثناء تكلفة تخصيص الاستثناء وإلقائه والتقاطه وتكلفة جمع البيانات المهملة اللاحقة لكائن الاستثناء، مما يعني أن التأثير اللحظي لرمي الاستثناء ليس بهذه الدرجة العالية.ومع طرح المزيد من الاستثناءات، يصبح جمع البيانات المهملة بشكل متكرر مشكلة، وبالتالي فإن التأثير الإجمالي للاستخدام المتكرر لتقنية ترميز طرح الاستثناءات سيكون مشابهًا للاختبار 8.05.

من الممارسات السيئة إضافة جملة الصيد فقط لإعادة الاستثناء.

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

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

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

catch كان مختلفا عن finally بمعنى أن الصيد كان مصممًا ليمنحك طريقة للتعامل مع/إدارة أو تفسير الخطأ بنفسه.فكر في الأمر كشخص يخبرك "مرحبًا ، لقد اشتعلت بعض الأشرار ، ماذا تريد مني أن أفعل لهم؟" بينما finally تم تصميمه للتأكد من أن الموارد الخاصة بك تم وضعها بشكل صحيح.فكر في شخص ما سواء كان هناك بعض الأشرار أم لا، فسوف يتأكد من أن ممتلكاتك لا تزال آمنة.

ويجب أن تسمح لهذين الاثنين بالعمل معًا من أجل الخير.

على سبيل المثال:

try
{
  StreamReader reader=new  StreamReader("myfile.txt");
  //do other stuff
}
catch(Exception ex){
 // Create log, or show notification
 generic.Createlog("Error", ex.message);
}
finally   // Will execute despite any exception
{
  reader.Close();
}

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

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

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

المشكلة في كتل المحاولة/التقاط التي تلتقط كافة الاستثناءات هي أن برنامجك الآن في حالة غير محددة في حالة حدوث استثناء غير معروف.وهذا يتعارض تمامًا مع قاعدة الفشل السريع - فأنت لا تريد أن يستمر برنامجك في حالة حدوث استثناء.قد تؤدي تجربة المحاولة/الالتقاط المذكورة أعلاه إلى اكتشاف OutOfMemoryExceptions، ولكن هذه بالتأكيد حالة لن يعمل فيها برنامجك.

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

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

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

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

لذا، فإن الفرق الرئيسي بين "catch" و"أخيرًا" هو أن محتويات الكتلة "أخيرًا" (مع بعض الاستثناءات النادرة) يمكن أخذها بعين الاعتبار مضمون للتنفيذ، حتى في مواجهة استثناء غير متوقع، في حين أن أي تعليمات برمجية تتبع عبارة "catch" (ولكن خارج عبارة "أخيرًا") لن تحمل مثل هذا الضمان.

وبالمناسبة، يقوم كل من Stream وStreamReader بتنفيذ IDisposable، ويمكن تغليفهما في كتلة "استخدام".تُعد الكتل "استخدام" المعادل الدلالي للمحاولة/أخيرًا (بدون "التقاط")، لذلك يمكن التعبير عن مثالك بشكل أكثر إيجازًا على النحو التالي:

using (StreamReader reader = new  StreamReader("myfile.txt"))
{
  int i = 5 / 0;
}

... والذي سيتم إغلاقه والتخلص من مثيل StreamReader عندما يخرج عن النطاق.أتمنى أن يساعدك هذا.

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

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