حين يحدث خطأ وقت التشغيل (ملف مفقود، نص ليس رقمًا، مفتاح غير موجود في قاموس) ترمي .NET استثناءً: كائنًا يصف الخطأ. ينتقل الاستثناء صعودًا في مكدّس الاستدعاءات حتى تعالجه كتلة catch. وإن لم يعالجه شيء يتوقف البرنامج ويطبع الخطأ.
try catch أساسية
غلّف الشيفرة التي قد تفشل في try، وعالج الفشل في catch:
المخرجات:
42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running
حين ترمي int.Parse("forty-two") تُتخطّى Console.WriteLine التي بعدها في كتلة try، وتُنفَّذ كتلة catch (FormatException)، وتستمر الحلقة. يمكنك حذف المتغيّر (catch (FormatException)) حين لا تحتاج إلى كائن الاستثناء.
لهذه الحالة بالذات أداة أفضل: تعيد int.TryParse(input, out int n) القيمة false بدل رمي استثناء. الاستثناءات للحالات التي لا تتوقعها الشيفرة؛ أما المدخلات التي تكون غالبًا غير صالحة فمتوقعة، فافحصها بدل التقاطها.
كيف ينتقل الاستثناء
الاستثناء المرمي في عمق سلسلة استدعاءات يفكّ كل دالة حتى يجد معالجًا مطابقًا. الشيفرة التي بعد الرمي في كل من تلك الدوال لا تُنفَّذ.
المخرجات:
OrderTotal finished
5.00
Caught KeyNotFoundException in Main
لا catch في PriceOf وOrderTotal، فيمرّ الاستثناء عبرهما مباشرة إلى Main. ضع معالجًا في المستوى الذي يعرف ما يفعله إزاء الفشل، وهو غالبًا ليس حيث حدث.
التقاط استثناءات محددة، بالترتيب الصحيح
يعالج بند catch نوعه وكل نوع مشتق منه. وحين توجد عدة بنود يفوز أول واحد يطابق، فرتّبها من الأكثر تحديدًا إلى الأعم:
المخرجات:
10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException
يرمي الاستدعاء الأخير OverflowException، التي لا يعالجها أي من البندين المحددين، فيعالجها البند العام catch (Exception e). ووضع catch (Exception) أولًا يجعل البنود التي بعده غير قابلة للوصول، ويرفض المترجم ذلك بالخطأ CS0160.
مرشّحات الاستثناء بـ when
أضافت C# 6 البند when: شرط يقرّر هل ينطبق بند catch. إن كان false يواصل وقت التشغيل البحث عن معالج آخر كأن البند غير موجود.
المخرجات:
Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401
تتيح لك المرشّحات التفرّع بحسب بيانات داخل الاستثناء دون التقاطه وإعادة رميه. وهي أيضًا الطريقة النظيفة لمعالجة نوعين غير مترابطين بشكل متطابق، كما يفعل البند الثالث. يُنفَّذ المرشّح قبل فكّ المكدّس، فيظل المصحّح أو ملف تفريغ الانهيار يُظهر الحالة الأصلية حين لا يطابق أي مرشّح.
finally: شيفرة تُنفَّذ دائمًا
تُنفَّذ كتلة finally حين يغادر التحكم try، سواء انتهت، أو عادت مبكرًا، أو رمت استثناءً. هناك مكان التنظيف. (تحفّظ واحد: إن لم يلتقط شيء في أي مكان الاستثناء، فقد تنتهي العملية دون تنفيذها.)
المخرجات:
Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error
في كل حالة تُطبع "Close connection" قبل أن تصل نتيجة الدالة إلى Main: تُنفَّذ finally بعد حساب قيمة return لكن قبل أن تعود الدالة فعلًا. ويمكن أن تملك try كتلة finally دون أي catch، فتنظّف مع ترك الاستثناء يستمر إلى المستدعي.
للكائنات التي تنفّذ IDisposable (الملفات والتدفقات والاتصالات) تكتب العبارة using هذه try/finally عنك.
إعادة الرمي: throw; مقابل throw e;
أحيانًا تسجّل كتلة catch شيئًا أو تحفظه ثم تترك الاستثناء يستمر. طريقة إعادة الرمي تقرّر هل ينجو أثر المكدّس:
المخرجات:
throw; trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False
تعامل throw e; الاستثناء كأنه رُمي حديثًا من كتلة catch، فتختفي من الأثر الإطارات التي تحتها، بما في ذلك الدالة التي حدث فيها الخطأ. أعد الرمي دائمًا بـ throw; مجردة. (السمة NoInlining موجودة فقط لأن JIT قد يدمج دالة بهذا الصغر في مستدعيها، فيخفيها من الأثرين.)
لإضافة سياق بدلًا من ذلك غلّف الاستثناء في واحد جديد ومرّر الأصلي كاستثناء داخلي: throw new ConfigException("Could not start the app", e);. يحتفظ الاستثناء الداخلي بأثر مكدّسه الخاص، وتطبع أدوات التسجيل السلسلة. وكتابة أنواع استثناءاتك الخاصة مشروحة في رمي الاستثناءات.
كائن Exception
كل استثناء مشتق من System.Exception. الأعضاء التي تستخدمها أكثر:
| العضو | ما يحمله |
|---|---|
Message | وصف مقروء للإنسان |
GetType().Name | نوع الاستثناء، مثل FormatException |
StackTrace | سلسلة استدعاءات الدوال عند نقطة الرمي |
InnerException | الاستثناء الذي سبّب هذا، أو null |
ToString() | النوع والرسالة والاستثناءات الداخلية وأثر المكدّس معًا |
سجّل e.ToString() بدل e.Message حين تريد تشخيص فشل لاحقًا: الرسالة وحدها نادرًا ما تقول أين كانت المشكلة. ولا تعرض e.ToString() على المستخدمين النهائيين.
أنواع الاستثناءات الشائعة
| الاستثناء | السبب النموذجي |
|---|---|
NullReferenceException | استدعاء عضو على مرجع null |
ArgumentNullException | مُرّرت null إلى دالة حيث تحتاج إلى قيمة |
ArgumentOutOfRangeException | وسيط أو فهرس قائمة خارج النطاق المسموح |
IndexOutOfRangeException | فهرس مصفوفة خارج حدودها |
FormatException | int.Parse وDateTime.Parse وأمثالهما على نص بصيغة خاطئة |
InvalidCastException | تحويل صريح إلى نوع ليس هو نوع الكائن |
InvalidOperationException | الكائن في حالة خاطئة للاستدعاء (تسلسل فارغ، مجموعة معدّلة) |
KeyNotFoundException | قراءة مفتاح قاموس غائب بالمفهرس |
DivideByZeroException | قسمة عدد صحيح أو decimal على صفر |
OverflowException | رقم مقروء أو تحويل مفحوص أو نتيجة حساب مفحوص لا يتّسع لها النوع |
FileNotFoundException، IOException | مشكلات نظام الملفات |
تعني NullReferenceException وIndexOutOfRangeException وInvalidCastException دائمًا تقريبًا خطأً برمجيًا. أصلح الشيفرة بدل التقاطها.
عدم ابتلاع الاستثناءات
كتلة catch الفارغة تخفي كل خطأ، بما في ذلك التي لم تتوقعها:
try
{
SaveOrder(order);
}
catch (Exception)
{
// nothing: the order silently was not saved
}
يستمر البرنامج في العمل كأن الحفظ نجح، ويضيع السبب الحقيقي. إرشادات تبقي معالجة الاستثناءات صادقة:
- لا تلتقط إلا ما تستطيع معالجته، في المستوى الذي يستطيع معالجته.
- إن التقطت للتسجيل فأعد الرمي بـ
throw;ما لم يكن البرنامج قادرًا فعلًا على المتابعة. - لا تلتقط
Exceptionإلا عند الحافة الخارجية:Main، أو معالج طلب، أو حلقة عامل. - فضّل
TryParseوTryGetValueوفحوصnullعلى الاستثناءات للحالات المتوقعة. الرمي بطيء مقارنة بالفحص، بينما كتلةtryالتي لا ترمي شيئًا لا تكلّف شيئًا تقريبًا.
أخطاء شائعة
catch (Exception)أولًا. تصبح البنود اللاحقة غير قابلة للوصول (CS0160).throw e;لإعادة الرمي. تمحو أثر المكدّس الأصلي؛ استخدمthrow;.- كتل catch فارغة. تختفي الأخطاء؛ على الأقل سجّل وأعد الرمي.
- استخدام الاستثناءات للتحكم في التدفق. تحقّق من المدخلات بـ
TryParseبدل التقاطFormatException. - عرض
e.Messageلاستثناء من الإطار على المستخدمين. تختلف الصياغة بين إصدارات .NET وهي مكتوبة للمطوّرين.
الأسئلة الشائعة
كيف تعمل try catch في C#؟
توضع الشيفرة التي قد تفشل في كتلة try. إن رمت عبارة هناك استثناءً تُتخطّى بقية الكتلة ويبحث وقت التشغيل عن بند catch يطابق نوعه نوع الاستثناء، أولًا في الدالة الحالية ثم في كل مستدعٍ. يُنفَّذ أول catch مطابق، ويستمر التنفيذ بعد جملة try كلها.
هل تُنفَّذ finally دائمًا في C#؟
تقريبًا دائمًا: بعد انتهاء كتلة try بشكل طبيعي، وبعد أن تعالج catch استثناءً، وبعد return أو break داخل الكتلة، وحين يمرّ استثناء في طريقه إلى catch أعلى في مكدّس الاستدعاءات. ولا تُنفَّذ حين تنتهي العملية أولًا: عملية مقتولة، أو Environment.FailFast، أو StackOverflowException، وفي .NET Core وما بعده استثناء لا يلتقطه شيء، فينهي العملية قبل تنفيذ كتل finally.
كيف ألتقط عدة استثناءات في C#؟
اكتب عدة بنود catch، الأكثر تحديدًا أولًا: catch (FileNotFoundException) قبل catch (IOException) قبل catch (Exception). يرفض المترجم البند الذي لا يمكن الوصول إليه أبدًا لأن بندًا سابقًا يلتقط نوعه أصلًا. ولمعالجة نوعين غير مترابطين بالطريقة نفسها استخدم مرشّحًا: catch (Exception e) when (e is FormatException || e is OverflowException).
ما الفرق بين throw وthrow ex في C#؟
داخل catch تعيد throw; رمي الاستثناء الحالي بأثر مكدّسه الأصلي. أما throw ex; فترمي الكائن نفسه لكنها تعيد ضبط أثر المكدّس إلى السطر الحالي، فتختفي من الأثر الدالة التي حدث فيها الخطأ فعلًا. استخدم throw;، أو غلّفه: throw new MyException("context", ex);.
ما هو مرشّح الاستثناء في C#؟
بند when بعد catch (C# 6 وما بعده): catch (HttpRequestException e) when (e.Message.Contains("404")). لا تُنفَّذ كتلة catch إلا إن كان الشرط true؛ وإلا يظل الاستثناء يبحث عن معالج آخر كأن البند غير موجود، ولا يُفكّ مكدّسه.
هل ألتقط Exception في C#؟
فقط عند حواف البرنامج: أعلى Main، أو معالج طلب، أو حلقة في الخلفية، حيث المهمة تسجيل الخطأ والمتابعة أو الخروج بنظافة. أما في عمق الشيفرة فالتقط الأنواع المحددة التي تستطيع معالجتها فعلًا، ودع كل ما عداها ينتشر.