تخطَّ إلى المحتوى

معالجة الأخطاء في C#‎: try و catch و throw

الدرس 10 من 11 · C#

اللاعب يكتب حروفاً مكان الرقم، وملف الحفظ محذوف، والقسمة على صفر تحدث فجأة. البرنامج الجيد لا ينهار أمام هذه المواقف، بل يتوقعها ويتعامل معها. في هذا الدرس ستتعلم نظام الاستثناءات (Exceptions) في C#‎: كيف تلتقط الخطأ، وكيف ترميه بنفسك، وكيف تضمن تنظيف الموارد مهما حدث.

عندما يحدث خطأ وقت التشغيل، تنشئ C#‎ كائناً يصف المشكلة و«ترميه». إذا لم يلتقطه أحد يتوقف البرنامج ويطبع رسالة مثل:

Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
at Program.<Main>$(String[] args) in Program.cs:line 2

في الرسالة ثلاث معلومات ثمينة: نوع الخطأ، ووصفه، ورقم السطر. اقرأها بهدوء قبل أي شيء.

ضع الكود الخطِر داخل 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 وصف المشكلة.

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

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 قراءة مفتاح غير موجود في قاموس

أنت أيضاً ترمي استثناءً عندما تستقبل دالتك قيمة غير منطقية:

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 لعملية لا تناسب حالة الكائن الحالية.

للأخطاء الخاصة ببرنامجك، أنشئ كلاساً يرث من 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.

يمكنك إضافة شرط إلى 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}");
}
}
الناتج
الصفحة غير موجودة
خطأ آخر: فشل الطلب برمز 500

الملفات والاتصالات موارد يجب إغلاقها بعد الاستخدام، حتى لو حدث خطأ. بدل كتابة 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
الملف غير موجود

عند الخروج من القوسين يُغلق الملف حتماً، سواء انتهى الكود بسلام أو رُمي استثناء في منتصفه.

  1. اكتب دالة Divide(int a, int b) ترمي ArgumentException برسالة عربية إذا كان b صفراً.
  2. استدعها داخل try/catch مرتين: بقيم سليمة ثم بصفر، وأضف finally يطبع «انتهت العملية».
  3. جرّب قراءة عنصر خارج حدود مصفوفة والتقط IndexOutOfRangeException.
  4. تحدٍّ: أنشئ InvalidAgeException، ودالة ParseAge(string text) ترميه إذا كان النص ليس رقماً أو كان العمر خارج المدى من 0 إلى 120.
  • catch فارغ: كتابة catch { } تبتلع الخطأ بصمت، فتبحث ساعات عن مشكلة لا أثر لها. سجّل رسالة على الأقل.
  • التقاط Exception العامة دائماً: تخفي أخطاء لم تتوقعها. التقط النوع المحدد الذي تعرف كيف تعالجه.
  • ترتيب خاطئ: وضع catch (Exception) قبل نوع أخص منه خطأ ترجمة، لأن القسم الثاني لن يصل إليه أحد.
  • throw e بدل throw: لإعادة رمي الخطأ من داخل catch اكتب throw; وحدها. كتابة throw e; تمسح مسار الخطأ الأصلي فيصعب تتبعه.

سؤال

متى يُنفَّذ الكود داخل finally؟

سؤال

ما فائدة عبارة using مع الملفات؟

  • try للكود الخطِر، و catch لخطة الطوارئ، و finally لما يُنفَّذ دائماً.
  • التقط النوع المحدد، ورتّب الأقسام من الأخص إلى الأعم.
  • throw new ... ترمي استثناءً، والاستثناء الخاص يرث من Exception.
  • when تضيف شرطاً للالتقاط، و using تغلق الموارد تلقائياً.
  • للأخطاء المتوقعة استخدم الفحص مثل TryParse بدل الاستثناءات.