C#: معالجة الاستثناءات
1. الآلية الأساسية لـ try/catch/finally
تستخدم لغة C# ثلاث كلمات رئيسية — try وcatch وfinally — لبناء آلية معالجة الاستثناءات الخاصة بها. يُوضع الكود الذي قد يُحدث استثناءً في كتلة try، بينما تقوم كتلة catch باصطياد الاستثناء ومعالجته، أما كتلة finally فتُنفَّذ دائمًا بغض النظر عما إذا كان قد حدث استثناء أم لا، وتُستخدم عادةً لتنظيف الموارد.
try
{
int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
finally
{
Console.WriteLine("finally block always executes");
}
Caught exception: Attempted to divide by zero.
finally block always executes
(1) كتل الصيد المتعددة
يمكنك معالجة أنواع الاستثناءات المختلفة بالترتيب. وينبغي وضع أنواع الاستثناءات الأكثر تحديدًا قبل الأنواع الأكثر عمومية.
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}");
}
Index out of bounds: Index was outside the bounds of the array.
2. أنواع الاستثناءات الشائعة
| نوع الاستثناء | سيناريو التشغيل |
|---|---|
FormatException |
تنسيق السلسلة غير صحيح، على سبيل المثال: int.Parse("abc") |
NullReferenceException |
الوصول إلى أحد عناصر كائن null |
IndexOutOfRangeException |
مؤشر المصفوفة خارج النطاق |
DivideByZeroException |
عدد صحيح مقسوم على الصفر |
OverflowException |
تجاوز سعة الحساب (في سياق خاضع للفحص) |
FileNotFoundException |
الملف غير موجود |
ArgumentException |
حجة طريقة غير صالحة |
▶ مثال
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");
}
Format exception: string cannot be converted to integer
Null reference exception: object is null
3. التسلسل الهرمي لفئات الاستثناءات
علاقة التوريث بين جميع فئات الاستثناءات هي كما يلي:
Object
└─ Exception
└─ SystemException
├─ FormatException
├─ NullReferenceException
├─ IndexOutOfRangeException
├─ DivideByZeroException
├─ OverflowException
├─ FileNotFoundException
└─ ArgumentException
└─ ArgumentNullException
4. خصائص فئة الاستثناءات
توفر فئة Exception الخصائص التالية الشائعة الاستخدام للحصول على معلومات تفصيلية عن الاستثناءات:
| العقار | الوصف |
|---|---|
Message |
نص يمكن قراءته بواسطة الإنسان يصف الاستثناء |
StackTrace |
معلومات مكدس الاستدعاءات في وقت حدوث الاستثناء |
InnerException |
الاستثناء الداخلي الذي تسبب في الاستثناء الحالي |
Source |
اسم التطبيق أو الكائن الذي أطلق الاستثناء |
TargetSite |
الطريقة التي أطلقت الاستثناء |
▶ مثال
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}");
}
Message: Attempted to divide by zero.
Source: ConsoleApp
TargetSite: Void Main()
5. إلقاء الاستثناءات باستخدام الكلمة «throw»
استخدم الكلمة الرئيسية throw لإلقاء استثناء بشكل فعلي. عند إعادة إلقاء الاستثناء، تحافظ throw; على تتبع المكدس الأصلي، بينما تعيد throw ex; تعيين المكدس. تجنب استخدام الخيار الأخير.
▶ مثال
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);
}
Age cannot be negative (Parameter 'age')
(1) الفرق بين الأمر «throw» و«throw ex»
void InnerMethod()
{
throw new InvalidOperationException("Internal error");
}
void OuterMethod()
{
try
{
InnerMethod();
}
catch (Exception ex)
{
throw;
}
}
try
{
OuterMethod();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
at InnerMethod() in Program.cs:line 2
at OuterMethod() in Program.cs:line 10
throw; لإعادة إلقاء الاستثناءات. فهذا يحافظ على مكدس الاستدعاءات بالكامل، مما يسهل تحديد السبب الجذري. أما throw ex; فيقتصر على الطريقة الحالية، مما يؤدي إلى فقدان معلومات موقع الاستثناء الأصلي.
6. مرشحات الاستثناءات
أدخلت لغة C# 6 مرشحات الاستثناءات باستخدام جملة when، مما يسمح بإضافة شروط إلى catch. ولا يتم التقاط الاستثناء إلا عندما تكون نتيجة تقييم الشرط هي true.
▶ مثال
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}");
}
Caught timeout exception, preparing to retry
7. فئات الاستثناءات المخصصة
يمكنك إنشاء فئات استثناء مخصصة عن طريق الوراثة من Exception. وعادةً ما يتعين عليك تنفيذ ثلاثة منشئات: منشئ بدون معلمات، ومنشئ بمعلمة «رسالة»، ومنشئ بمعلمتي «رسالة» و«استثناء داخلي».
▶ مثال
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}");
}
Insufficient balance: current balance 100, required 200
Balance: 100, attempted withdrawal: 200
8. أفضل الممارسات في معالجة الاستثناءات
(1) استثناءات محددة، وليس مجرد استثناء
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) لا تقم بـ«ابتلاع» الاستثناءات
try
{
File.WriteAllText("data.txt", content);
}
catch (Exception)
{
}
يخفي الكود أعلاه جميع الأخطاء، وهو ما يُعد ممارسة سيئة. كحد أدنى، يجب عليك تسجيل الخطأ أو إعادة إطلاقه.
(3) استخدم الكلمة «finally» لتحرير الموارد
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 عند نهاية نطاقها، مما يضمن تحرير الموارد حتى في حالة حدوث استثناء.
using (var fs = new FileStream("data.txt", FileMode.Open))
{
int b = fs.ReadByte();
}
using تعادل العبارة try-finally مع استدعاء Dispose()، وهي الطريقة المفضلة لإدارة الموارد.
(5) تغليف الاستثناءات باستخدام InnerException
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.📖 ملخص
- يحيط الكتلة
tryبالكود الذي قد ينتج عنه خطأ، بينما تلتقط الكتلةcatchالاستثناءات، أما الكتلةfinallyفتُنفَّذ دائمًا - تشمل أنواع الاستثناءات الشائعة
FormatException،NullReferenceException،IndexOutOfRangeException،DivideByZeroException، وما إلى ذلك. - توفر الفئة
ExceptionالخصائصMessageوStackTraceوInnerExceptionوغيرها من الخصائص throw;يحافظ على المكدس؛throw ex;يعيد تعيين المكدس — استخدم الخيار الأول- ترث فئات الاستثناءات المخصصة
Exceptionوتنفذ منشئات قياسية - يوفر مرشح الاستثناءات
catch ... when (...)إمكانية التقاط مشروط - لا تقم بتجاهل الاستثناءات، بل قم بالتعامل مع أنواع محددة منها، واستخدم
usingلإدارة الموارد، وقم بتغليف الاستثناءات باستخدامInnerException
📝 تمارين
- اكتب دالة
int SafeParse(string s)تستخدمtry-catchلمعالجةFormatException، وتُرجع0، وتُظهر تحذيرًا في حالة فشل عملية التحليل - اكتب استثناءً مخصصًا
InvalidGradeExceptionيحتوي على خاصيةGrade، يتم إطلاقه عندما لا تقع الدرجة ضمن النطاق 0-100 - اكتب كودًا يوضح الفرق بين
throw;وthrow ex;، مع عرض قيمتيStackTraceوملاحظة الفرق بينهما - استخدم مرشح الاستثناءات
whenلتنفيذ ما يلي: لا يتم التقاطFileNotFoundExceptionإلا عندما يبدأ مسار الملف بـ"config"، وإلا فليتم السماح بانتشاره - اكتب طريقة لقراءة الملف تستخدم العبارة
usingلضمان تحريرStreamReaderبشكل صحيح، مع معالجة الحالات المحتملة لـFileNotFoundExceptionوIOException