معالجة الأخطاء في C#: try و catch و throw
اللاعب يكتب حروفاً مكان الرقم، وملف الحفظ محذوف، والقسمة على صفر تحدث فجأة. البرنامج الجيد لا ينهار أمام هذه المواقف، بل يتوقعها ويتعامل معها. في هذا الدرس ستتعلم نظام الاستثناءات (Exceptions) في C#: كيف تلتقط الخطأ، وكيف ترميه بنفسك، وكيف تضمن تنظيف الموارد مهما حدث.
ما هو الاستثناء؟
Section titled “ما هو الاستثناء؟”عندما يحدث خطأ وقت التشغيل، تنشئ C# كائناً يصف المشكلة و«ترميه». إذا لم يلتقطه أحد يتوقف البرنامج ويطبع رسالة مثل:
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero. at Program.<Main>$(String[] args) in Program.cs:line 2في الرسالة ثلاث معلومات ثمينة: نوع الخطأ، ووصفه، ورقم السطر. اقرأها بهدوء قبل أي شيء.
try و catch
Section titled “try و catch”ضع الكود الخطِر داخل try، وخطة الطوارئ داخل catch:
try{ int zero = 0; int result = 10 / zero; Console.WriteLine("لن يُطبع هذا السطر");}catch (DivideByZeroException e){ Console.WriteLine($"خطأ حسابي: {e.Message}");}Console.WriteLine("البرنامج مستمر");خطأ حسابي: Attempted to divide by zero.البرنامج مستمرعند حدوث الخطأ يقفز التنفيذ فوراً إلى catch وتُهمل بقية أسطر try، ثم يكمل البرنامج طريقه. المتغير e هو كائن الاستثناء، و e.Message وصف المشكلة.
الأمر مثل نقطة الحفظ في الألعاب: إذا سقطت الشخصية، لا تنتهي اللعبة، بل تعود وتكمل.
أكثر من catch، ثم finally
Section titled “أكثر من catch، ثم finally”string[] inputs = { "25", "abc", "0" };foreach (string input in inputs){ try { int number = int.Parse(input); Console.WriteLine($"100 / {number} = {100 / number}"); } catch (FormatException) { Console.WriteLine($"«{input}» ليس رقماً"); } catch (DivideByZeroException) { Console.WriteLine("لا يمكن القسمة على صفر"); } finally { Console.WriteLine($"-- انتهت معالجة {input}"); }}100 / 25 = 4-- انتهت معالجة 25«abc» ليس رقماً-- انتهت معالجة abcلا يمكن القسمة على صفر-- انتهت معالجة 0- C# تجرّب أقسام
catchبالترتيب وتنفّذ أول قسم يطابق نوع الخطأ. - إذا لم تحتج كائن الخطأ، اكتب النوع وحده بدون اسم متغير.
finallyيُنفَّذ دائماً: سواء نجح الكود أو فشل. نضع فيه أعمال التنظيف.
هذه أشهر الاستثناءات التي ستقابلها:
| الاستثناء | متى يحدث |
|---|---|
NullReferenceException |
استخدام متغير قيمته null |
IndexOutOfRangeException |
رقم عنصر خارج حدود المصفوفة |
FormatException |
تحويل نص ليس رقماً بـ Parse |
KeyNotFoundException |
قراءة مفتاح غير موجود في قاموس |
throw: ارمِ استثناءك
Section titled “throw: ارمِ استثناءك”أنت أيضاً ترمي استثناءً عندما تستقبل دالتك قيمة غير منطقية:
static double Withdraw(double balance, double amount){ if (amount <= 0) { throw new ArgumentException("المبلغ يجب أن يكون أكبر من صفر"); } return balance - amount;}
try{ Console.WriteLine(Withdraw(100, 30)); Console.WriteLine(Withdraw(100, -5)); Console.WriteLine("لن نصل إلى هنا");}catch (ArgumentException e){ Console.WriteLine($"مرفوض: {e.Message}");}70مرفوض: المبلغ يجب أن يكون أكبر من صفرthrow توقف الدالة فوراً وترسل الخطأ إلى من استدعاها. ومن الأنواع الجاهزة المفيدة: ArgumentException لمعامل غير صالح، و InvalidOperationException لعملية لا تناسب حالة الكائن الحالية.
استثناء خاص بك
Section titled “استثناء خاص بك”للأخطاء الخاصة ببرنامجك، أنشئ كلاساً يرث من Exception:
class InsufficientFundsException : Exception{ public double Missing { get; }
public InsufficientFundsException(double missing) : base($"الرصيد لا يكفي، ينقصك {missing}") { Missing = missing; }}
class Account{ public double Balance { get; private set; } = 100;
public void Withdraw(double amount) { if (amount > Balance) { throw new InsufficientFundsException(amount - Balance); } Balance -= amount; Console.WriteLine($"تم السحب، الرصيد الآن {Balance}"); }}var account = new Account();try{ account.Withdraw(30); account.Withdraw(200);}catch (InsufficientFundsException e){ Console.WriteLine(e.Message); Console.WriteLine($"المبلغ الناقص: {e.Missing}");}تم السحب، الرصيد الآن 70الرصيد لا يكفي، ينقصك 130المبلغ الناقص: 130الاسم الواضح للاستثناء يشرح المشكلة، ويمكنه أن يحمل بيانات إضافية مثل Missing. والعُرف أن ينتهي اسمه بكلمة Exception.
الفلترة بـ when
Section titled “الفلترة بـ when”يمكنك إضافة شرط إلى catch، فلا يلتقط الخطأ إلا إذا تحقق:
int[] codes = { 404, 500 };foreach (int code in codes){ try { throw new InvalidOperationException($"فشل الطلب برمز {code}"); } catch (InvalidOperationException e) when (e.Message.Contains("404")) { Console.WriteLine("الصفحة غير موجودة"); } catch (InvalidOperationException e) { Console.WriteLine($"خطأ آخر: {e.Message}"); }}الصفحة غير موجودةخطأ آخر: فشل الطلب برمز 500using: تنظيف تلقائي
Section titled “using: تنظيف تلقائي”الملفات والاتصالات موارد يجب إغلاقها بعد الاستخدام، حتى لو حدث خطأ. بدل كتابة finally يدوياً، عبارة using تغلق المورد تلقائياً عند نهاية النطاق:
string path = Path.Combine(Path.GetTempPath(), "notes-demo.txt");
using (var writer = new StreamWriter(path)){ writer.WriteLine("السطر الأول"); writer.WriteLine("السطر الثاني");}
Console.WriteLine(File.ReadAllLines(path).Length);File.Delete(path);
try{ File.ReadAllText(path);}catch (FileNotFoundException){ Console.WriteLine("الملف غير موجود");}2الملف غير موجودعند الخروج من القوسين يُغلق الملف حتماً، سواء انتهى الكود بسلام أو رُمي استثناء في منتصفه.
جرّب بنفسك
Section titled “جرّب بنفسك”- اكتب دالة
Divide(int a, int b)ترميArgumentExceptionبرسالة عربية إذا كانbصفراً. - استدعها داخل
try/catchمرتين: بقيم سليمة ثم بصفر، وأضفfinallyيطبع «انتهت العملية». - جرّب قراءة عنصر خارج حدود مصفوفة والتقط
IndexOutOfRangeException. - تحدٍّ: أنشئ
InvalidAgeException، ودالةParseAge(string text)ترميه إذا كان النص ليس رقماً أو كان العمر خارج المدى من 0 إلى 120.
أخطاء شائعة
Section titled “أخطاء شائعة”- catch فارغ: كتابة
catch { }تبتلع الخطأ بصمت، فتبحث ساعات عن مشكلة لا أثر لها. سجّل رسالة على الأقل. - التقاط Exception العامة دائماً: تخفي أخطاء لم تتوقعها. التقط النوع المحدد الذي تعرف كيف تعالجه.
- ترتيب خاطئ: وضع
catch (Exception)قبل نوع أخص منه خطأ ترجمة، لأن القسم الثاني لن يصل إليه أحد. - throw e بدل throw: لإعادة رمي الخطأ من داخل
catchاكتبthrow;وحدها. كتابةthrow e;تمسح مسار الخطأ الأصلي فيصعب تتبعه.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
متى يُنفَّذ الكود داخل finally؟
finally يعمل في الحالتين، ولذلك نضع فيه أعمال التنظيف.
سؤال
ما فائدة عبارة using مع الملفات؟
using تضمن استدعاء Dispose وإغلاق المورد عند نهاية النطاق في كل الأحوال.
الخلاصة
Section titled “الخلاصة”tryللكود الخطِر، وcatchلخطة الطوارئ، وfinallyلما يُنفَّذ دائماً.- التقط النوع المحدد، ورتّب الأقسام من الأخص إلى الأعم.
throw new ...ترمي استثناءً، والاستثناء الخاص يرث منException.whenتضيف شرطاً للالتقاط، وusingتغلق الموارد تلقائياً.- للأخطاء المتوقعة استخدم الفحص مثل
TryParseبدل الاستثناءات.