التقاط الاستثناءات نصف معالجة الأخطاء؛ والنصف الآخر رمي الاستثناء الصحيح. الاستثناء المختار جيدًا يخبر المستدعي بالضبط ما الخطأ وهل كان خطأه أم حالة البرنامج. تغطي هذه الصفحة جانب الرمي؛ وتغطي try catch المعالجة.
عبارة throw وجمل الحراسة
تأخذ throw كائن استثناء. يتوقف تنفيذ الدالة هناك، ويبحث الاستثناء عن معالج صعودًا في مكدّس الاستدعاءات. أشيع استخدام هو جملة الحراسة: فحوص في أعلى الدالة ترفض المدخلات غير الصالحة قبل أداء أي عمل.
المخرجات:
ArgumentOutOfRangeException for parameter 'amount'
InvalidOperationException: The account is frozen.
Balance: 100
تنتج nameof(amount) النص "amount" وتبقى صحيحة إن أُعيدت تسمية المعامل. تخزّنه استثناءات الوسائط في ParamName، الذي تستخدمه الأدوات والسجلات للإشارة إلى الوسيط السيئ.
تبقي جمل الحراسة بقية الدالة بسيطة: بعد الفحوص تستطيع الشيفرة افتراض مدخلات صالحة. وتفشل أيضًا في موضع الخطأ بدل ترك قيمة سيئة تنتقل وتسبّب NullReferenceException مربكًا بعد ثلاث دوال.
أي نوع استثناء ترمي
أعد استخدام نوع مدمج حين يصف الحالة؛ فالمستدعون يعرفون أصلًا كيف يعالجونه.
| الحالة | ارمِ |
|---|---|
وسيط إلزامي قيمته null | ArgumentNullException |
| وسيط خارج النطاق المسموح (كمية سالبة، فهرس بعد النهاية) | ArgumentOutOfRangeException |
| وسيط غير صالح بطريقة أخرى (اسم فارغ، معرّف مشوّه) | ArgumentException |
| الاستدعاء غير صالح في حالة الكائن الحالية | InvalidOperationException |
العملية غير مدعومة أبدًا في هذا النوع (Add في مجموعة للقراءة فقط) | NotSupportedException |
| الدالة لم تُكتب بعد | NotImplementedException |
استُخدم كائن بعد Dispose | ObjectDisposedException |
| نفد وقت عملية محدودة بزمن | TimeoutException |
الخط الفاصل بين الصفوف الثلاثة الأولى وInvalidOperationException هو من يحتاج إلى تغيير شيء. يقول استثناء الوسيط "استدعِ هذا بشكل مختلف". ويقول InvalidOperationException "الاستدعاء سليم، لكن ليس الآن".
لا ترمِ Exception أو SystemException أو ApplicationException مباشرة: لا يستطيع المستدعون التقاطها دون التقاط كل شيء آخر أيضًا. ولا ترمِ NullReferenceException أو IndexOutOfRangeException أو StackOverflowException بنفسك أيضًا؛ فوقت التشغيل يحجزها للأخطاء البرمجية الفعلية.
تعابير throw
قبل C# 7 كانت throw عبارة فقط. ومنذ C# 7 يمكنها الظهور أيضًا كتعبير في ثلاثة أماكن، وهذا يحوّل الفحوص الشائعة إلى سطر واحد:
المخرجات:
Ana <ana@example.com>
Null: name
ArgumentException: email
لاحظ أن ArgumentNullException مشتق من ArgumentException، فبند catch (ArgumentException) موضوع أولًا سيلتقط حالة null أيضًا. ولهذا السبب نفسه يهمّ ترتيب بنود catch حين تعالج الاثنين.
ThrowIfNull وأخواتها (.NET 6 وما بعده)
تضيف .NET الحديثة دوال مساعدة ساكنة تكتب الفحص والرمي عنك، مع التقاط اسم المعامل تلقائيًا:
public void Ship(Order order, int quantity, string address)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // .NET 8
ArgumentException.ThrowIfNullOrWhiteSpace(address); // .NET 8
ObjectDisposedException.ThrowIf(disposed, this); // .NET 7
// ...
}
تتصرف مثل if وthrow المكتوبتين يدويًا، وتبقي كل جملة حراسة في سطر واحد. وعلى الأهداف الأقدم اكتب شكل if المعروض سابقًا.
كتابة صنف استثناء مخصص
أنشئ نوع استثناء خاصًا بك حين يحتاج المستدعون إلى التقاط هذا الفشل المحدد بشكل منفصل، أو حين يحتاج المعالج إلى بيانات لا يستطيع نص الرسالة حملها جيدًا.
المخرجات:
Cannot withdraw 25 from a balance of 15.
Short by 10
الأعراف:
- ينتهي الاسم بـ
Exception. - يشتق من
Exception(أو من نوع مدمج أكثر تحديدًا حين يكون حالة خاصة منه، مثلInvalidOperationException). - له المُنشئات القياسية الثلاثة: بلا شيء، والرسالة، والرسالة مع استثناء داخلي. أضف مُنشئاتك فوقها.
- البيانات الإضافية في خصائص للقراءة فقط تُضبط في المُنشئ. عندها يستطيع المعالج التصرف بحسب
e.Requestedبدل تحليل الرسالة.
التغليف باستثناء داخلي
حين يجب أن يظهر فشل منخفض المستوى كفشل أعلى مستوى، غلّفه. يُحفظ الأصلي كـ InnerException، فلا تضيع أي معلومة:
المخرجات:
Setting 'port' must be a number, got '80a'.
Caused by: FormatException
يتعامل المستدعي الآن بمصطلحات الإعدادات، التي يفهمها، وتسجيل e.ToString() يطبع السلسلة كلها بما في ذلك FormatException وأثر مكدّسه. لا تغلّف إلا حين تضيف معنى؛ فتغليف كل استثناء في MyAppException عام لا يفعل سوى جعل المعالجات تحفر في InnerException.
الرمي مقابل إعادة نتيجة
الاستثناءات للإخفاقات التي لا يتوقعها المستدعي في التشغيل العادي. أما النتائج الروتينية، مثل بحث لا يجد شيئًا غالبًا أو مدخلات مستخدم غير صالحة غالبًا، فعُرف .NET لها هو نمط Try: أعد bool وسلّم القيمة عبر معامل out.
public bool TryWithdraw(decimal amount, out string error)
{
if (amount > Balance) { error = "Insufficient funds."; return false; }
Balance -= amount;
error = null;
return true;
}
أنواع كثيرة تقدّم الاثنين: ترمي int.Parse، وتعيد int.TryParse القيمة false؛ ويرمي dict[key]، وتعيد dict.TryGetValue القيمة false. رمي استثناء يكلّف أكثر بكثير من إعادة قيمة، فيجب ألا يقع على مسار يُنفَّذ آلاف المرات في الثانية. راجع ref وout لمعاملات out.
كتابة رسائل جيدة
يقرأ رسالة الاستثناء مطوّر ينظر في سجل. اجعلها تذكر ما الخطأ، وحيث يكون ذلك آمنًا القيمة المخالفة: "Quantity must be between 1 and 99, got 0." أفضل من "Invalid input.". اكتب جملًا كاملة، وأبعد الأسرار مثل كلمات المرور والرموز عن الرسائل، لأنها تنتهي في ملفات السجلات.
أخطاء شائعة
- رمي
Exceptionنفسها. لا يستطيع المستدعون التقاطها انتقائيًا؛ استخدم نوعًا محددًا. - تمرير الرسالة مكان اسم المعامل. يأخذ
new ArgumentNullException("name")اسم المعامل؛ والرسالة تأتي ثانيًا. - أسماء معاملات مكتوبة حرفيًا. استخدم
nameof(param)كي تبقى صحيحة عند إعادة التسمية. - استثناءات مخصصة بلا معنى إضافي. إن كان نوع مدمج يناسب فاستخدمه.
- فقدان الخطأ الأصلي عند التغليف. مرّره دائمًا كاستثناء داخلي.
الأسئلة الشائعة
كيف أرمي استثناءً في C#؟
أنشئ كائن استثناء وارمه: throw new ArgumentException("Amount must be positive", nameof(amount));. يتوقف التنفيذ عند ذلك السطر وينتقل الاستثناء صعودًا في مكدّس الاستدعاءات إلى أقرب catch مطابق. اختر أكثر نوع مدمج تحديدًا يصف المشكلة، أو نوعًا مخصصًا حين يحتاج المستدعون إلى معالجة هذه الحالة بشكل منفصل.
كيف أنشئ استثناءً مخصصًا في C#؟
اشتق صنفًا من Exception ينتهي اسمه بـ Exception، وامنحه المُنشئات القياسية: واحد بلا وسائط، وواحد يأخذ رسالة، وواحد يأخذ رسالة واستثناءً داخليًا، كل منها يستدعي مُنشئ base(...) المطابق. أضف خصائص للقراءة فقط لأي بيانات يحتاجها المعالج، مثل معرّف طلب أو رصيد.
متى أرمي ArgumentException ومتى InvalidOperationException؟
ارمِ ArgumentException (أو ArgumentNullException / ArgumentOutOfRangeException) حين يمرّر مستدعٍ قيمة سيئة: الإصلاح أن يستدعي الدالة بشكل مختلف. وارمِ InvalidOperationException حين تكون الوسائط سليمة لكن الكائن في حالة خاطئة للاستدعاء، مثل القراءة من اتصال مغلق أو السحب من حساب مجمّد.
ما هو تعبير throw في C#؟
منذ C# 7 يمكن استخدام throw كتعبير في ثلاثة أماكن: بعد ??، وكأي من فرعي ?:، وكجسم عضو بجسم تعبيري أو تعبير lambda. مثلًا تُسند _name = name ?? throw new ArgumentNullException(nameof(name)); أو ترمي في سطر واحد.
ماذا تفعل ArgumentNullException.ThrowIfNull؟
دالة مساعدة ساكنة أُضيفت في .NET 6: ترمي ArgumentNullException.ThrowIfNull(customer); استثناء ArgumentNullException مع ملء اسم المعامل تلقائيًا حين تكون customer هي null، ولا تفعل شيئًا في غير ذلك. وأضافت الإصدارات اللاحقة دوال مساعدة مشابهة مثل ArgumentException.ThrowIfNullOrEmpty (.NET 7) وArgumentOutOfRangeException.ThrowIfNegative (.NET 8).