تعلّم البرمجة بلغة كوتلن (28): الاستثناءات Exceptions

تعلّم البرمجة بلغة كوتلن (28): الاستثناءات Exceptions
أستمع الى المقال

تحدثنا في الدروس السابقة، عن كيفية قراءة مُدخلات المستخدم والطباعة عبر استخدام دوال من مكتبة كوتلن القياسية. وشرحنا أيضًا، كيف يُمكن لكوتلن إنتاج برنامج يستطيع العمل على حاسوب جافا الافتراضي JVM. في هذا الدرس، سنستعرض الأخطاء الغير متوقعة التي قد تحدث أثناء عمل برنامجنا، وطرق معالجتها.

الكثير من المعلومات في هذا الدرس، تعتمد بشكل كلّي أو جزئي على المعلومات المذكورة في الدروس أعلاه. بالإضافة إلى ذلك، سيتم أيضاً إضافة رابط إلى كل درس آخر تم استقاء معلومة منه، عند ذكرها في هذا الدرس، للمراجعة.

الأخطاء Errors في البرامج:

بعد كتابة شفرة برنامج كوتلن، قد تحدث أخطاء مختلفة أثناء تجميع وترجمة الشفرة إلى لغة الآلة (بايت كود byte-code) أو عند تنفيذها وتشغيلها كبرنامج. لذلك، يمكننا تقسيم هذه الأخطاء المحتملة إلى مجموعتين: أخطاء وقت الترجمة compile-time errors، و أخطاء وقت التشغيل run-time errors.

  1. أخطاء وقت الترجمة Compile-Time Errors:

عند حدوث هذه الأخطاء، لن يتم تجميع وترجمة البرنامج من قبل مترجم كوتلن Compiler. كمثال عليها:

  • أخطاء في بناء الجملة Syntax Errors: مثل كتابة كلمة رئيسية غير صحيحة أو خطأ مطبعي في اسم دالة، أو نسيان كتابة أقواس … الخ. 
  • تضمين اسم حزمة package غير صحيح.
  • استدعاء دالة غير موجودة.

كل هذه الأخطاء سيكتشفها المترجم حالًا أثناء عمله في تجميع و ترجمة الشفرة. وإذا كنا نستخدم بيئة تطوير متكاملة IDE مثل برنامج IntelliJ، فسينبهنا إليها مباشرةً دون حتى أن نحاول ترجمة وتنفيذ شفرة البرنامج.

ولفهم هذا النوع من الأخطاء، لننظر إلى المثال التالي:

يحتوي البرنامج أعلاه، على خطأين يمنعان البرنامج من الترجمة. وهما أن اسم دالة الطباعة ()println ينقصه حرف n، لذلك لن يتعرف عليها المترجم. وأن جملة Hello World، عبارة عن سلسلة من المحارف من النوع String. وفي كوتلن، يجب وضع سلاسل المحارف من النوع String بين علامتي تنصيص مزدوجة (” “)، وليس بين علامتي تنصيص منفردة (‘ ‘)، ﻷن الأخيرة مخصصة للمحارف.

  1. أخطاء وقت التشغيل Run-Time Errors:

تعرف أيضًا باسم Bugs. وهي الأخطاء التي تحدث أثناء تشغيل البرنامج. وستؤدي إلى تصرف برنامجنا بشكل غير متوقع أو حتى إلى تحطمه وإيقاف تنفيذه. ولن نحصل على مساعدة من المترجم، ﻷن دوره إنتهى مع ترجمة الشفرة إلى لغة الآلة. وسنعرف السبب في الفقرات أدناه.

تنقسم أخطاء وقت التشغيل، إلى نوعين فرعيين:

  • الأخطاء المنطقية Logic Errors: حينما ينتج برنامجنا نتيجة خاطئة غير التي نريدها. كمثال، حينما يطبع برنامجنا كلمة Hi، بدلًا عن كلمة Hello. لا يعرف المترجم ما كنا نريد طباعته حقًا. لذا تجنب هذا الخطأ هو مسؤولية المبرمج.
  • الاستثناءات التي لم تتم معالجتها Unhandled Exceptions: وهي الأخطاء الغير متوقعة. مثلًا، إذا كان لدينا برنامج آلة حاسبة، وحاول مستخدم البرنامج قسمة عدد على الرقم صفر. لا يمكن القسمة على صفر كما نعرف في الرياضيات. هذه من الأخطاء التي ستُنهي عمل البرنامج وتتسبب في تحطمه. وﻷنه خطأ غير متوقع ويحدث أثناء تنفيذ البرنامج، وقد يتسبب فيه مستخدم البرنامج، تم تسميته بـ استثناء Exception.

ما هو الاستثناء Exception:

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

السبب في حدوث استثناء:

الأشياء التي تتسبب في حدوث استثناء كثيرة. ولكن الأهم أنها غير متوقعة. كمثال، إذا كان لدينا برنامج آلة حاسبة لجمع الأعداد التي التي يُدخلها المستخدم، كالتالي:

برنامج بسيط نقوم فيه بقراءة مُدخلات نصية من المستخدم وتحويلها إلى أعداد عبر دالة ()toInt. ثم نجري عملية جمع على العددين ونطبع النتيجة. هل من الممكن حدوث مشكلة في مثل هذا البرنامج البسيط؟ حسنًا، فلنرى ماذا أدخل المستخدم في الصورة التالية:

تعامل المستخدم مع الأمر حرفيًا، وأدخل “واحد” بدلاً عن 1. ولأن دالة ()toInt لم تستطع تحويل النص “واحد” إلى عدد صحيح، نتج عن ذلك خطأ استثنائي تسبب في إيقاف وتحطم البرنامج بالكامل. 

ونلاحظ أن البرنامج تم تنفيذه بالفعل من قبل المترجم في البداية بدون إظهار أية أخطاء. هذا ﻷن المترجم لا يعرف بعد ما سيُدخله مستخدم البرنامج.

قراءة نص الاستثناء:

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

Exception in thread “main” java.lang.NumberFormatException: For input string: “واحد”
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:668)
at java.base/java.lang.Integer.parseInt(Integer.java:786)
at exceptions.ExceptionsKt.main(Exceptions.kt:6)
at exceptions.ExceptionsKt.main(Exceptions.kt)

من المهم جداً قراءة وفهم محتويات هذه الرسالة حتى نستطيع حل المشكلة.

تفكيك رسالة الاستثناء:

سنقرأ الاستثناء سطرًا سطرًا، تمهيدًا لفهمه ومعالجته لاحقًا. وسنبدأ بالسطر الأول من الرسالة، كالتالي:

Exception in thread “main” java.lang.NumberFormatException: For input string: “واحد”

هذا الجزء “Exception in thread “main من السطر، يُخبرنا أن هناك استثناء حدث في الخيط الحاسوبي thread الذي يحمل اسم main. بالطبع ليس علينا فهم معنى “خيط حاسوبي” في هذه المرحلة، والذي سنستعرضه بالطبع في الدروس القادمة. في هذا الدرس، يكفي أن نفهم المقصود من هذا السطر.

وهذا الجزء java.lang.NumberFormatException، يُخبرنا باسم الاستثناء. والذي يُمثله الصنف NumberFormatException الموجود داخل حِزمة lang في حاسوب جافا الافتراضي java. وكما يظهر من اسم الصنف، أن هناك مشكلة لها علاقة بتنسيق الأعداد.

أما هذا الجزء  “واحد” :For input string، فهو يُخبرنا بالمُدخلات المُتسببة في الاستثناء.

تتبع المكدِّس Stack Trace:

بالنسبة للسطور التي تبدأ بـ at، فهي تُظهر الدوال التي كان يتم استدعاؤها أثناء حدوث الاستثناء، تُعرف باسم stack trace. هذه السطور، تدلنا بالضبط عن مكان حدوث الاستثناء.  كالتالي:

at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:668)
at java.base/java.lang.Integer.parseInt(Integer.java:786)
at exceptions.ExceptionsKt.main(Exceptions.kt:6)
at exceptions.ExceptionsKt.main(Exceptions.kt)

ما يهمنا في هذه السطور، هو أول سطر ظهر به ملف يخص برنامجنا. وفي رسالة الاستثناء أعلاه، هو هذا السطر:

at exceptions.ExceptionsKt.main(Exceptions.kt:6)

هذا الجزء Exceptions.kt:6 من السطر، هو مكان حدوث الاستثناء بالضبط. المقصود ب Exceptions.kt هو اسم الملف في برنامجنا، والرقم 6، هو رقم السطر في الملف. وهو الجزء الذي يظهر أيضًا، باللون الأزرق في الصورة أعلاه من برنامج IntelliJ.

أثَر حدوث استثناء على مستخدم البرنامج:

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

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

أنواع الاستثناءات:

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

النوع الفرعي والنوع الأعلى Subtype and Supertype:

يتم تنظيم الأنواع في كوتلن، في تسلسل هرمي للعلاقات بين النوع الفرعي Subtype والنوع الأعلى Supertype. لفهم هذين المصطلحين، فلننظر إلى المثال التالي:

في الصورة أعلاه، يُمثل كلًا من القهوة Coffee والشاي Tea نوع من أنواع: الشراب Drink. وبالتالي، يرث كلًا منهما، كل خصائص النوع Drink. في كوتلن، يمكن اعتبار أن الـ Coffee والـ Tea نوعين فرعيين Subtypes مرتبطان بالنوع الأعلى Supertype المسمى  Drink.

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

التسلسل الهرمي للاستثناءات:

قياساً على ما ورد في الفقرة السابقة، يكون شكل التسلسل الهرمي ﻷنواع الاستثناءات في كوتلن، كالتالي:

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

يحتوي النوع Throwable على نوعين فرعيين مباشرين: Error و Exception. في هذا الدرس، سنركز على Exception الذي تنتمي إليه كل الاستثناءات. ويمكن اعتبار النوع RuntimeException نوع فرعي من Exception. وفي ذات الوقت، لدى RuntimeException أنواع فرعية. منها النوع الذي رأيناها سابقًا في هذا الدرس، NumberFormatException، و النوع ArithmeticException والذي سيتم إظهاره حينما يقوم المستخدم بعملية رياضية خاطئة مثلاً، وغيرهما من الاستثناءات التي تظهر أثناء فترة تشغيل البرنامج Runtime.

معالجة الاستثناءات Exceptions Handling:

كما رأينا في المثال أعلى هذا الدرس، حينما حدث الاستثناء توقف سير التنفيذ العادي للبرنامج. ﻷننا كما أوضحنا سابقًا، لا نريد حدوث هذا، سنقوم بكتابة شفرة للتعامل مع الاستثناء ومنعه من إيقاف البرنامج. وتمتلك كوتلن آلية لمعالجة الاستثناءات، إذ يمكن التعامل معها عبر كتلتي try و catch.

كتلة try-catch:

تكون أبسط صورة لهاتين الكتلتين، كالتالي:

كتابة الكلمة المفتاحية try، ثم قوسين معقوفين نضع بهما التعليمات البرمجية أو السطر الذي يمكن أن يسبب استثناء. بعدها الكلمة المفتاحية catch، وبين قوسيها نضع معامل (متغير) من نوع الاستثناء الذي يمكن أن يحدث. لتتضح الصورة أكثر، دعونا نعدّل على برنامجنا السابق، كالتالي:

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

نلاحظ أيضًا، أننا قمنا بتضمين الصنف Exception، وهو النوع الأعلى Supertype لكل الاستثناءات. ثم وضعنا معامل بين قوسي كتلة catch، يُمثل الاستثناء الذي حدث. الآن أي استثناء يحدث، ستقوم الكتلة بمعالجته، ﻷنه على كل حال سيكون نوع فرعي Subtype من النوع Exception.

عدد لا نهائي من كتل catch:

إذا قمنا بتعديل برنامجنا ليشمل العمليات الحسابية الأربعة: الجمع، الطرح، الضرب والقسمة، نجد أن هناك استثناء آخر يمكن أن يحدث في برنامجنا الذي ما زال بسيطًا. وهو من الممكن أن يحاول المستخدم القسمة على صفر. هذا شئ غير مقبول في الرياضيات وكذلك برنامجنا لن يعرف كيفية القسمة على صفر. 

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

عدّلنا على الشفرة وأضفنا العمليات الحسابية الأربع. ولتجنّب إطالة شفرة الدالة ()main، أنشأنا دالة جديدة مهمتها القيام بالعمليات الحسابية وإعادة النتيجة، عبر استخدام كتلة if. ثم أضفنا كتلة catch أخرى لالتقاط الاستثناء الذي سينتج إذا حاول المستخدم القسمة على صفر.

ولكن كما نرى في الصورة، عند حدوث الاستثناء تم طباعة الرسالة الموجودة في كتلة catch الأولى، مع تجاهل كتلة catch الثانية. هذا حدث ﻷننا نضع النوع الأعلى لكل الاستثناءات Exception في الكتلة الأولى. وﻷن استثناء القسمة على صفر أيضًا ينتمي إليه، تم معالجته في الكتلة الأولى.

هذا ليس ما نريده. نحن نريد عند حدوث استثناء عدم إدخال رقم صحيح، أن يتم طباعة الرسالة: “الرجاء إدخال أرقام فقط. كمثال: 1، 2، 3…”. وعند حدوث استثناء القسمة على صفر، يتم طباعة الرسالة: “لا يُمكن القسمة على صفر”. ﻷن هكذا ستكون الرسائل محددة ومفهومة أكثر للمستخدم. إذًا، ماهو الحل؟ حسنًا، الحل هو تخصيص نوع فرعي مناسب لكل استثناء يحدث في الشفرة.

تخصيص نوع الاستثناء:

تُوفر كوتلن، مجموعة كبيرة من أنواع الاستثناءات. سنستفيد من ذلك لتعديل شفرة برنامجنا أعلاه. فمثلًا، للتعامل مع استثناء عدم إدخال رقم صحيح، يمكن أن نضيف في إحدى كُتل catch، معامل يُمثل الصنف NumberFormatException. ولاستثناء القسمة على صفر، سنضيف في كتلة أخرى، معامل يُمثل الصنف ArithmeticException، كالتالي:

هكذا نكون قد خصصنا رسالة مناسبة لكل استثناء قد يحدث. وعند تنفيذ البرنامج هذه المرة، ستلتقط كل كتلة catch الاستثناء المناسب لما بين قوسيها وتعالجه، كالتالي:

استثناء إدخال خاطئ لرقم:

استثناء القسمة على صفر:

نكتفي بهذا القدر عن الاستثناءات في هذا الدرس، ﻷن ما تعلمناه فيه عنها، سيكون كافيًا  لفهم التعامل معها. بعض الميزات الإضافية مثل: إنشاء استثناءات خاصة بنا، التعامل مع كتلة try-catch كتعبير يُعيد قيمة، وغيرها سنستعرضها في درسٍ لاحق في هذه الدورة.

هذا الدرس هو جزء من سلسلة تعليم مبادئ البرمجة بلغة كوتلن. لمُتابعة الدروس منذ البداية ومُشاهدة فهرس المحتويات يمكنك الانتقال إلى الدرس الأول من هنا.

هل أعجبك المحتوى وتريد المزيد منه يصل إلى صندوق بريدك الإلكتروني بشكلٍ دوري؟
انضم إلى قائمة من يقدّرون محتوى إكسڤار واشترك بنشرتنا البريدية.