C#: معالجة الاستثناءات

1. الآلية الأساسية لـ try/catch/finally

تستخدم لغة C# ثلاث كلمات رئيسية — try وcatch وfinally — لبناء آلية معالجة الاستثناءات الخاصة بها. يُوضع الكود الذي قد يُحدث استثناءً في كتلة try، بينما تقوم كتلة catch باصطياد الاستثناء ومعالجته، أما كتلة finally فتُنفَّذ دائمًا بغض النظر عما إذا كان قد حدث استثناء أم لا، وتُستخدم عادةً لتنظيف الموارد.

CSHARP
try
{
    int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Caught exception: {ex.Message}");
}
finally
{
    Console.WriteLine("finally block always executes");
}
TEXT 📖 للعرض فقط
Caught exception: Attempted to divide by zero.
finally block always executes

(1) كتل الصيد المتعددة

يمكنك معالجة أنواع الاستثناءات المختلفة بالترتيب. وينبغي وضع أنواع الاستثناءات الأكثر تحديدًا قبل الأنواع الأكثر عمومية.

CSHARP
try
{
    int[] arr = { 1, 2, 3 };
    Console.WriteLine(arr[10]);
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine($"Index out of bounds: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"General exception: {ex.Message}");
}
TEXT 📖 للعرض فقط
Index out of bounds: Index was outside the bounds of the array.

2. أنواع الاستثناءات الشائعة

نوع الاستثناء سيناريو التشغيل
FormatException تنسيق السلسلة غير صحيح، على سبيل المثال: int.Parse("abc")
NullReferenceException الوصول إلى أحد عناصر كائن null
IndexOutOfRangeException مؤشر المصفوفة خارج النطاق
DivideByZeroException عدد صحيح مقسوم على الصفر
OverflowException تجاوز سعة الحساب (في سياق خاضع للفحص)
FileNotFoundException الملف غير موجود
ArgumentException حجة طريقة غير صالحة

▶ مثال

CSHARP
try
{
    int num = int.Parse("hello");
}
catch (FormatException)
{
    Console.WriteLine("Format exception: string cannot be converted to integer");
}

string s = null;
try
{
    Console.WriteLine(s.Length);
}
catch (NullReferenceException)
{
    Console.WriteLine("Null reference exception: object is null");
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Format exception: string cannot be converted to integer
Null reference exception: object is null

3. التسلسل الهرمي لفئات الاستثناءات

علاقة التوريث بين جميع فئات الاستثناءات هي كما يلي:

TEXT 📖 للعرض فقط
Object
  └─ Exception
       └─ SystemException
            ├─ FormatException
            ├─ NullReferenceException
            ├─ IndexOutOfRangeException
            ├─ DivideByZeroException
            ├─ OverflowException
            ├─ FileNotFoundException
            └─ ArgumentException
                 └─ ArgumentNullException

4. خصائص فئة الاستثناءات

توفر فئة Exception الخصائص التالية الشائعة الاستخدام للحصول على معلومات تفصيلية عن الاستثناءات:

العقار الوصف
Message نص يمكن قراءته بواسطة الإنسان يصف الاستثناء
StackTrace معلومات مكدس الاستدعاءات في وقت حدوث الاستثناء
InnerException الاستثناء الداخلي الذي تسبب في الاستثناء الحالي
Source اسم التطبيق أو الكائن الذي أطلق الاستثناء
TargetSite الطريقة التي أطلقت الاستثناء

▶ مثال

CSHARP
try
{
    int zero = 0;
    int result = 100 / zero;
}
catch (Exception ex)
{
    Console.WriteLine($"Message: {ex.Message}");
    Console.WriteLine($"Source: {ex.Source}");
    Console.WriteLine($"TargetSite: {ex.TargetSite}");
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Message: Attempted to divide by zero.
Source: ConsoleApp
TargetSite: Void Main()

5. إلقاء الاستثناءات باستخدام الكلمة «throw»

استخدم الكلمة الرئيسية throw لإلقاء استثناء بشكل فعلي. عند إعادة إلقاء الاستثناء، تحافظ throw; على تتبع المكدس الأصلي، بينما تعيد throw ex; تعيين المكدس. تجنب استخدام الخيار الأخير.

▶ مثال

CSHARP
void CheckAge(int age)
{
    if (age < 0)
    {
        throw new ArgumentException("Age cannot be negative", nameof(age));
    }
    Console.WriteLine($"Age: {age}");
}

try
{
    CheckAge(-5);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Age cannot be negative (Parameter 'age')

(1) الفرق بين الأمر «throw» و«throw ex»

CSHARP
void InnerMethod()
{
    throw new InvalidOperationException("Internal error");
}

void OuterMethod()
{
    try
    {
        InnerMethod();
    }
    catch (Exception ex)
    {
        throw;
    }
}

try
{
    OuterMethod();
}
catch (Exception ex)
{
    Console.WriteLine(ex.StackTrace);
}
TEXT 📖 للعرض فقط
   at InnerMethod() in Program.cs:line 2
   at OuterMethod() in Program.cs:line 10
💡 نصيحة: استخدم دائمًا throw; لإعادة إلقاء الاستثناءات. فهذا يحافظ على مكدس الاستدعاءات بالكامل، مما يسهل تحديد السبب الجذري. أما throw ex; فيقتصر على الطريقة الحالية، مما يؤدي إلى فقدان معلومات موقع الاستثناء الأصلي.


6. مرشحات الاستثناءات

أدخلت لغة C# 6 مرشحات الاستثناءات باستخدام جملة when، مما يسمح بإضافة شروط إلى catch. ولا يتم التقاط الاستثناء إلا عندما تكون نتيجة تقييم الشرط هي true.

▶ مثال

CSHARP
try
{
    throw new HttpRequestException("Network timeout, please retry later");
}
catch (HttpRequestException ex) when (ex.Message.Contains("timeout"))
{
    Console.WriteLine("Caught timeout exception, preparing to retry");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Other network exception: {ex.Message}");
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Caught timeout exception, preparing to retry

7. فئات الاستثناءات المخصصة

يمكنك إنشاء فئات استثناء مخصصة عن طريق الوراثة من Exception. وعادةً ما يتعين عليك تنفيذ ثلاثة منشئات: منشئ بدون معلمات، ومنشئ بمعلمة «رسالة»، ومنشئ بمعلمتي «رسالة» و«استثناء داخلي».

▶ مثال

CSHARP 📖 للعرض فقط
class InsufficientBalanceException : Exception
{
    public decimal Balance { get; }
    public decimal Amount { get; }

    public InsufficientBalanceException()
        : base("Insufficient balance") { }

    public InsufficientBalanceException(string message)
        : base(message) { }

    public InsufficientBalanceException(string message, Exception innerException)
        : base(message, innerException) { }

    public InsufficientBalanceException(decimal balance, decimal amount)
        : base($"Insufficient balance: current balance {balance}, required {amount}")
    {
        Balance = balance;
        Amount = amount;
    }
}

class BankAccount
{
    public decimal Balance { get; private set; }

    public BankAccount(decimal balance)
    {
        Balance = balance;
    }

    public void Withdraw(decimal amount)
    {
        if (amount > Balance)
        {
            throw new InsufficientBalanceException(Balance, amount);
        }
        Balance -= amount;
        Console.WriteLine($"Withdrawal successful, balance: {Balance}");
    }
}

var account = new BankAccount(100);
try
{
    account.Withdraw(200);
}
catch (InsufficientBalanceException ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine($"Balance: {ex.Balance}, attempted withdrawal: {ex.Amount}");
}
44 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)
TEXT 📖 للعرض فقط
Insufficient balance: current balance 100, required 200
Balance: 100, attempted withdrawal: 200

8. أفضل الممارسات في معالجة الاستثناءات

(1) استثناءات محددة، وليس مجرد استثناء

CSHARP
try
{
    int value = int.Parse(input);
    Console.WriteLine(100 / value);
}
catch (FormatException)
{
    Console.WriteLine("Input is not a valid integer");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Cannot input zero");
}
⚠️ ملاحظة: لا تستخدم كتل catch فارغة لإخفاء الاستثناءات، لأن ذلك يجعل تشخيص المشكلات أمراً صعباً.

(2) لا تقم بـ«ابتلاع» الاستثناءات

CSHARP
try
{
    File.WriteAllText("data.txt", content);
}
catch (Exception)
{
}

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

(3) استخدم الكلمة «finally» لتحرير الموارد

CSHARP
FileStream fs = null;
try
{
    fs = new FileStream("data.txt", FileMode.Open);
    int b = fs.ReadByte();
}
finally
{
    fs?.Dispose();
}

(4) استخدم عبارة using بدلاً من try-finally

ينبغي للموارد التي تُنفِّذ واجهة IDisposable أن تُفضِّل استخدام العبارة using، التي تستدعي تلقائيًّا Dispose عند نهاية نطاقها، مما يضمن تحرير الموارد حتى في حالة حدوث استثناء.

CSHARP
using (var fs = new FileStream("data.txt", FileMode.Open))
{
    int b = fs.ReadByte();
}
📌 نقطة أساسية: العبارة using تعادل العبارة try-finally مع استدعاء Dispose()، وهي الطريقة المفضلة لإدارة الموارد.

(5) تغليف الاستثناءات باستخدام InnerException

CSHARP
try
{
    SaveToFile();
}
catch (IOException ioEx)
{
    throw new ApplicationException("Failed to save data", ioEx);
}

باستخدام InnerException، يتم الحفاظ على معلومات الاستثناء الأصلية دون فقدان السياق.


❓ أسئلة شائعة

س ما الفرق بين throw; وthrow ex;؟
ج throw; يحافظ على مسار المكدس الأصلي؛ أما throw ex; فيعيد تعيين المكدس إلى الطريقة الحالية. استخدم throw; عند تصحيح الأخطاء.
س متى لا يتم تنفيذ الكتلة finally؟
ج عندما يتم إنهاء البرنامج قسريًّا (على سبيل المثال، Environment.FailFast) أو عند حدوث StackOverflowException، قد لا يتم تنفيذ finally.
س هل يمكن استخدام try-finally بدون catch؟
ج نعم. لا يعالج هذا النهج الاستثناء، لكنه يضمن تنظيف الموارد؛ ويستمر الاستثناء في الانتقال إلى المستويات الأعلى.
س هل يجب أن ترث فئات الاستثناءات المخصصة من Exception؟
ج نعم، يجب أن ترث جميع فئات الاستثناءات من Exception (بشكل مباشر أو غير مباشر) حتى يتم التقاطها بواسطة catch.
س ما هي مزايا مرشح الاستثناءات when؟
ج لا تلتقط جملة when الاستثناء عندما لا تتطابق الشرط، مما يسمح للاستثناء بالاستمرار في الانتشار. وهذا أكثر كفاءة وأكثر وضوحًا من الناحية الدلالية مقارنةً بفحص الشروط داخل catch.

📖 ملخص

📝 تمارين

  1. اكتب دالة int SafeParse(string s) تستخدم try-catch لمعالجة FormatException، وتُرجع 0، وتُظهر تحذيرًا في حالة فشل عملية التحليل
  2. اكتب استثناءً مخصصًا InvalidGradeException يحتوي على خاصية Grade، يتم إطلاقه عندما لا تقع الدرجة ضمن النطاق 0-100
  3. اكتب كودًا يوضح الفرق بين throw; وthrow ex;، مع عرض قيمتي StackTrace وملاحظة الفرق بينهما
  4. استخدم مرشح الاستثناءات when لتنفيذ ما يلي: لا يتم التقاط FileNotFoundException إلا عندما يبدأ مسار الملف بـ "config"، وإلا فليتم السماح بانتشاره
  5. اكتب طريقة لقراءة الملف تستخدم العبارة using لضمان تحرير StreamReader بشكل صحيح، مع معالجة الحالات المحتملة لـ FileNotFoundException وIOException
Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%