البحث في الموقع
المحتوى عن 'finally'.
-
لا تخلو أيّ لغة برمجة محترمة من وسيلة لمعالجة الأخطاء. تحتوي لغة سي شارب على آليّة قويّة لمعالجة الأخطاء. تشبه هذه الآلية إلى حدّ ما تلك المستخدمة في لغة Java. هناك نوعان أساسيّان من الأخطاء التي قد تنتج عن البرنامج: النوع الأوّل هي أخطاء أثناء التصميم، أي أثناء كتابة الشيفرة، وهي أخطاء يمكن اكتشافها بسهولة من خلال مترجم سي شارب الذي يتعرّف عليها ويعطيك وصفًا عنها، وقد يزوّدك أيضًا ببعض المقترحات للتخلّص منها. بالنسبة للنوع الثاني من الأخطاء فهي التي تنتج أثناء تنفيذ البرنامج runtime errors. توجد الكثير من الأسباب لحدوث مثل هذه الأخطاء. فمن القسمة على صفر إلى القراءة من ملف غير موجود أو حتى استخدام متغيّر مصرّح على أنّه من نوع مرجعيّ ولكن لم تتم تهيئته بعد بمرجع إلى كائن. عند حدوث مثل هذه الأخطاء ترمي بيئة التنفيذ المشتركة CLR استثناءً exception يحتوي على معلومات بخصوص الخطأ الذي حدث، فإذا كنت مستعدًّا لالتقاط هذا الاستثناء فستنجو، وإلّا سيتوقّف برنامجك عن العمل بصورة مفاجئة. التقاط استثناء من خلال عبارة try-catch إذا صادفتك عبارة برمجيّة تحتوي على عمليّة حسابيّة أو على استدعاء لتابع آخر، أو أيّ شيء قد يثير الريبة في نفسك، فمن الممكن مراقبتها أثناء تنفيذ البرنامج باستخدام عبارة try-catch. تتألّف هذه العبارة من قسمين: القسم الأوّل هو قسم المراقبة try، والقسم الثاني هو قسم الالتقاط catch. الشكل "الأبسط" لهذه العبارة هو التالي: try { //عبارة برمجيّة مريبة } catch(Exception exp) { //هنا يُلتقط الاستثناء وتتمّ معالجته } يمكن أن يحوي قسم try على عبارات برمجيّة بقدر ما ترغب. عندما يصادف البرنامج أثناء التنفيذ خطأً ما، سيتوقّف التنفيذ عند العبارة التي سبّبت الخطأ، ثم ينتقل فورًا إلى قسم الالتقاط catch. لاحظ معي أنّ قسم catch سيُمرَّر إليه وسيط من الصنف Exception. الصنف Exception موجود ضمن نطاق الاسم System، وهو الصنف الأب لجميع الاستثناءات. إذ أنّ أي استثناء مهما كانت صفته (بما فيها الاستثناءات التي يمكنك أن تكتبها أنت) يجب أن ترث من هذا الصنف. بعد حدوث الاستثناء والانتقال إلى قسم catch، سيحتوي الوسيط exp على معلومات حول مكان حدوث الاستثناء وسبب حدوثه، وغيرها من المعلومات التي قد تكون مفيدة لمستخدم البرنامج. تجدر الملاحظة بأنّ البرنامج لن يدخل إلى القسم catch أبدًا ما لم يحدث استثناء ضمن القسم try. لنرى الآن البرنامج Lesson16_01 الذي سيعرّفنا على الاستثناءات بشكل عمليّ. يحتوي هذا البرنامج البسيط على عبارة try-catch وحيدة سنعمل من خلالها على توليد خطأ بشكل مقصود أثناء التنفيذ، وسنتعلّم كيفيّة المعالجة. 1 using System; 2 3 namespace Lesson16_01 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int x = 5; 10 int y = 0; 11 int result; 12 13 try 14 { 15 result = x / y; 16 } 17 catch(Exception exp) 18 { 19 Console.WriteLine("The following error has occurred:"); 20 Console.WriteLine(exp.Message); 21 } 22 23 Console.WriteLine("Good Bye!"); 24 } 25 } 26 } من الواضح أنّ هذا البرنامج يتجّه لأن يجري عمليّة قسمة على صفر في السطر 15 ضمن القسم try. عند وصول البرنامج إلى السطر 15 وإجراء عمليّة القسمة هذه، سيتولّد استثناء يؤدّي إلى انتقال التنفيذ مباشرةً إلى قسم catch في السطر 17. في قسم catch يعرض البرنامج معلومات عن هذا الخطأ باستخدام الخاصيّة Message لكائن الحدث exp. في النهاية يعرض البرنامج في السطر 23 رسالة توديعيّة للمستخدم. نفّذ البرنامج لتحصل على الخرج التالي: The following error has occurred: Attempted to divide by zero. Good Bye! جرّب الآن تغيير قيمة المتغيّر y لتصبح 1 مثلًا وأعد تنفيذ البرنامج. ستلاحظ ظهور الرسالة التوديعيّة فقط على الشاشة، أي أنّه لم يحدث أي استثناء هذه المرّة. على كلّ الأحوال لا ينصح باستخدام معالجة الاستثناءات من أجل حالة القسمة على صفر في البرنامج السابق. ملاحظة: في الواقع يمكن الاستغناء عن الوسيط الذي يمرّر إلى قسم catch بشكل كامل، وفي هذه الحالة لن يكون بإمكانك الحصول على معلومات حول الاستثناء المُلتقط. سيبدو شكل عبارة try-catch على الشكل التالي: try { //عبارة برمجيّة مريبة } catch { //هنا يُلتقط الاستثناء وتتمّ معالجته } من الممكن استخدام هذا الأسلوب إذا كنّا على يقين حول طبيعة الخطأ الذي سيحدث. عبارة try-catch أكثر تطورا تصادفنا في بعض الأحيان حالات يكون من الضروري معها مراقبة أكثر من عبارة برمجيّة مريبة ضمن قسم try. لقد اتفقنا أنّه عند حدوث أيّ استثناء ضمن قسم try سينتقل التنفيذ إلى قسم catch. ولكن كيف سنميّز العبارة التي سبّبت هذا الاستثناء في قسم try؟ توجد العديد من الأصناف التي ترث من الصنف Exception والتي يُعتبر كلّ منها استثناءً مخصّصًا أكثر للمشكلة التي قد تحدث. فمثًلا كان من الممكن في البرنامج Lesson16_01 السابق أن نستخدم الصنف DivideByZeroException بدلًا من الصنف Exception في عبارة catch، وذلك لأنّه يرث (بشكل غير مباشر) من الصنف Exception، وسيعمل البرنامج كما هو متوقّع. ولكن في هذه الحالة لن تستطيع catch التقاط سوى الاستثناءات التي تنتج عن القسمة على صفر. في كثير من الحالات قد تتسبّب العبارات البرمجيّة الموجودة في قسم try باستثناءات متنوّعة لا توجد علاقة فيما بينها. مما يفرض علينا استخدام الصنف Exception لكي نلتقط بشكل مؤكّد أي استثناء قد يصدر عنها، أو أن تساعدنا سي شارب في هذا الخصوص! في الحقيقة الخيار الثاني هو الأفضل وهو جاهز. يمكننا في الواقع إضافة أقسام catch أخرى بقدر ما نرغب بعد قسم try. سيوضّح البرنامج Lesson16_02 هذه الفكرة من خلال فتح ملف نصي ومحاولة قراءة محتوياته. لاحظ أنّنا سنستخدم هنا نطاق الاسم System.IO. 1 using System; 2 using System.IO; 3 4 namespace Lesson16_02 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 string contents; 11 12 Try 13 { 14 using (StreamReader sr = new StreamReader("myfile.txt")) 15 { 16 contents = sr.ReadLine(); 17 } 18 19 Console.WriteLine("This file contains {0} characters.", contents.Length); 20 } 21 catch(NullReferenceException nullExp) 22 { 23 Console.WriteLine("The file does not contain any data."); 24 } 25 catch(FileNotFoundException notFoundExp) 26 { 27 Console.WriteLine("File: {0} not found!", notFoundExp.FileName); 28 } 29 } 30 } 31 } يستخدم هذا البرنامج قسميّ catch. القسم الأوّل (الأسطر من 21 إلى 24) يلتقط استثناءً من النوع NullReferenceException وهذا يحدث عند محاولة استدعاء تابع أو خاصيّة من متغيّر يحتوي على null بدلًا من مرجع لكائن حقيقي. أمّا القسم الثاني (الأسطر من 25 إلى 28) فهو يلتقط استثناءً من النوع FileNotFoundException والذي يحدث عند محاولة القراءة من ملف غير موجود. نفّذ البرنامج السابق وستحصل على الرسالة التالية في الخرج: File: C:\\Users\Husam\documents\visual studio 2015\Projects\Lesson16_02\Lesson16_02\bin\Debug\myfile.txt not found! وهذا طبيعي تمامًا لأنّني لم أنشئ الملف myFile.txt في هذا المسار. لاحظ كيف يضيف الصنف FileNotFoundException خاصيّة جديدة له وهي FileName (السطر 27) من النوع string التي تحوي مسار الملف مع اسمه. أنشئ الآن الملف myFile.txt واتركه فارغًا، ثمّ ضعه ضمن نفس المجلّد الذي يحوي الملف التنفيذي للبرنامج (موجود ضمن bin\Debug\). أعد تنفيذ البرنامج وستحصل على الرسالة التالي: The file does not contain any data. السبب في ظهور هذه الرسالة هو الاستثناء NullReferenceException وذلك لأنّنا حاولنا الوصول إلى الخاصيّة Length (تعطينا عدد المحارف الموجودة ضمن متغيّر نصي) من المتغيّر النصي contents رغم أنّه يحتوي على null (تذكّر بأنّنا تركنا الملف myFile.txt فارغًا). اذهب إلى الملف myFile.txt الذي أنشأناه قبل قليل، واكتب بعض الكلمات ضمنه واحفظ الملف، ثمّ أعد تنفيذ البرنامج Lesson16_02 من جديد. يجب الآن أن تحصل على رسالة تخبرك بعدد الأحرف التي كتبتها ضمن الملف. ملاحظة: يجب الانتباه إلى ترتيب أقسام catch. فلو كان مثلًا أوّل قسم catch موجود بعد قسم try يلتقط استثناءً من النوع Exception فعندها لن يستطيع أي قسم لاحق التقاط أي استثناء، لأنّ جميع الاستثناءات سيلتقطها هذا القسم الأوّل. السبب في ذلك أنّ الصنف Exception هو الأب العام لجميع أصناف الاستثناءات الأخرى، فيمكن له التقاطها. عبارة try-catch-final يمكن إضافة قسم أخير لعبارة try-catch اسمه final. وكما يوحي اسمه، فهذا القسم يمكن له أن يحتوي على عبارات برمجيّة سيتمّ تنفيذها بعد أن يدخل البرنامج إلى القسم try العائد له. وذلك سواءً أحدث استثناء ضمن try أم لم يحدث. تكمن فائدة وجود هذا القسم، في أنّه قد نواجه أحيانًا بعض الحالات التي تتطلّب إجراء بعض المهام عندما نفرغ من قسم try مثل إغلاق بعض المصادر المفتوحة، أو تحرير الذاكرة بشكل فوري وغيرها. الشرط الوحيد لاستخدام هذا القسم الاختياري هو أن يكون آخر قسم في عبارة try-catch. سنعدّل البرنامج Lesson16_02 ليدعم القسم final. انظر البرنامج Lesson16_03. 1 using System; 2 using System.IO; 3 4 namespace Lesson16_03 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 string contents; 11 12 try 13 { 14 using (StreamReader sr = new StreamReader("myfile.txt")) 15 { 16 contents = sr.ReadLine(); 17 } 18 Console.WriteLine("This file contains {0} characters.", contents.Length); 19 } 20 catch(NullReferenceException nullExp) 21 { 22 Console.WriteLine("The file does not contain any data."); 23 } 24 catch(FileNotFoundException notFoundExp) 25 { 26 Console.WriteLine("File: {0} not found!", notFoundExp.FileName); 27 } 28 finally 29 { 30 Console.WriteLine("Good Bye!"); 31 } 32 } 33 } 34 } سواءً كان الملف myFile.txt موجودًا أم غير موجود، أو كان يحتوي على بيانات أم فارغاً، ستظهر العبارة !Good Bye على الشاشة. ملاحظة: من الأفضل دومًا أن تحاول عدم استخدام عبارة try-catch وأن تستخدم عبارة if لاختبار الحالات التي تواجهك قبل تنفيذها. استخدم try-catch إذا كان ذلك ضروريًّا. والسبب في ذلك أنّ عمليّة معالجة الأخطاء بشكل عام تتطلّب المزيد من الموارد المخصّصة للبرنامج، مما قد يؤثّر على أداء البرنامج في حال تمّ استخدامها بشكل غير مدروس. تمارين داعمة تمرين 1 عدّل البرنامج Lesson16_02 بحيث يمكن الاستغناء عن عبارة try-catch تمامًا. (تلميح: ستحتاج إلى استخدام عبارتيّ if في هذه الحالة). تمرين 2 لتكن لدينا الشيفرة التالية: string input = Console.ReadLine(); int t = int.Parse(input); تطلب الشيفرة السابقة من المستخدم أن يدخل عددًا على شكل نص لتعمل على تحويله إلى قيمة عدديّة باستخدام التابع int.Parse. المطلوب هو إضافة عبارة try-catch إلى الشيفرة السابقة لمعالجة استثناء ممكن الحدوث في حال أدخل المستخدم قيمة مثل "u" وهي لا يمكن تحويلها إلى قيمة عدديّة كما هو واضح. (تلميح: استخدام الاستثناء FormatException الذي يُعبّر عن هذه الحالة). الخلاصة تعرّفنا في هذا الدرس على كيفيّة معالجة الأخطاء التي تظهر أثناء تنفيذ البرنامج. في الحقيقة هذا الموضوع ذو شجون! وهناك الكثير ليقال، ولكن يكفي الآن أن تتعرّف على المبادئ الأساسيّة لالتقاط الاستثناءات، وكيفيّة معالجتها. يمكنك استخدام هذا الأسلوب في جميع التطبيقات التي تُنشئها باستخدام سي شارب، مثل تطبيقات الويب بأنواعها، وتطبيقات سطح المكتب، وحتى تطبيقات الأجهزة الذكيّة باستخدام تقنيّة Xamarin.