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

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

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

إذا كانت عملية التشخيص والتصحيح Debugging، هي عملية لإزالة الأخطاء البرمجية Bugs من البرنامج، فلابد أن تكون البرمجة، هي عملية وضعها فيه.

Edsger Wybe Dijkstra

دائمًا هناك إمكانية لحدوث الأخطاء:

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

يمكن لمترجم كوتلن أن يساعدنا، في اكتشاف الأخطاء الأساسية التي تحدث أثناء فترة تجميع وترجمة البرنامج Compile Time، عبر تحليل شفرة البرنامج، قبل ترجمتها. أمّا الأخطاء التي تحدث أثناء عمل البرنامج أي في فترة الـ Runtime، فهذه يجب أن نضع لها معالجات، أثناء عمل البرنامج.

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

إنشاء استثناء مُخصص:

قلنا في درس الاستثناءات، أن كل الاستثناءات تم تضمينها في هيكل هرمي واحد. وتنتمي كلها إلى النوع Exception، والذي بدوره ينتمي للنوع Throwable، وهو النوع في أعلى الهرم. وواحد من أهم أنواع الاستثناءات التي ترث من النوع Exception، هو النوع RuntimeException. يضم بدوره العديد من أنواع الاستثناءات، والتي يتم رميها إذا كان هناك استثناء أثناء عمل البرنامج في فترة الـ Runtime. مثل: النوع ArithmeticException، والنوع NumberFormatException، والنوع IndexOutOfBoundsException.

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

رمي استثناء Throwing exceptions:

كمثال، إذا كنا نريد استثناء مخصص، يتم رميه إذا كانت مدخلات المستخدم لا تتبع القواعد:

class UserAccountException : Exception()

أنشأنا صنف أسميناه UserAccountException وجعلناه يرث من الصنف الأب Exception. ويمكننا الآن استخدامه كالتالي:

fun main() {

    val username = readln()
    val age = readln().toInt()

    if (age < 18) {
        throw UserAccountException()
    }
}

يقرأ البرنامج مُدخلين من المستخدم. الأول اسم المستخدم username والثاني عمره age. فلنفترض أننا نريد من البرنامج أن يرمي استثناء إذا كان عمر المستخدم أقل من 18 عام. لذا في كتلة if، نتحقق مما إذا كان عمر المستخدم أقل من 18، إذا كان هذا صحيحًا أي true، يجب أن يتم رمي استثناء عبر استخدام الكلمة المفتاحية throw.

هذه الكلمة، تتطلب منا أن نستخدم معها كائن من النوع Throwable أو الأنواع الفرعية المُشتقة منه، حتى لو كانت لا ترث منه مباشرةً، مثل النوع الخاص بنا UserAccountException. لأن Exception نفسه يرث من النوع Throwable، بالتالي يعتبر النوع الخاص بنا، ابنًا لـ Throwable أيضًا. لذلك استطعنا استخدامه مع الكلمة throw.

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

استثناء يظهر به اسم نوع الاستثناء فقط ولا وجود لنص توضيحي

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

تخصيص رسالة الاستثناء:

لدى النوع الأب Exception عدة بواني صنف. أحدها يستقبل معامل من النوع String. لذلك، يمكننا استخدام كائن من النوع Exception مع الكلمة throw، بالطريقة التالية أيضًا:

throw Exception("some message")

حيث يمكننا وضع مكان النص some message، أي نص نرغب في ظهوره عند حدوث الاستثناء. ويمكننا بالطبع تمرير النص من الصنف الابن إلى الصنف الأب عند الوراثة. لذا، سنعدل الشفرة كالتالي:

class UserAccountException(message: String): Exception(message)

أضفنا معامل جديد من النوع String أسميناه message، إلى نوع الاستثناء الجديد الخاص بنا UserAccountException. الآن، عند استخدام كائن من هذا الاستثناء، يجب أن نضع نص بين أقواسه، وإلا سيحدث خطأ. وعندما استقبال النص من قبل النوع الجديد الابن، سيمرر نفس النص إلى النوع الأب Exception. وسيتم التعامل مع النص في النوع الأب وإعادة النص مع رسالة الخطأ، متى ما حدث الاستثناء.

ويمكننا استخدام النوع الجديد كما سبق، فقط هذه المرة سنضع نص بين أقواسه:

fun main() {

    val username = readln()
    val age = readln().toInt()

    if (age < 18) {
        throw UserAccountException("User is very young")
    }
}

وبمجرد إدخال رقم أقل من 18 كقيمة للمتغير age، سيتم رمي الاستثناء التالي:

Exception in thread “main” preventingFailure.exceptionsHandling.UserAccountException: User is very young

at preventingFailure.exceptionsHandling.MainKt.main(Main.kt:10)

at preventingFailure.exceptionsHandling.MainKt.main(Main.kt)

ما يهمنا في هذا الدرس في الرسالة أعلاه، هو السطر التالي:

preventingFailure.exceptionsHandling.UserAccountException: User is very young

كل ما يسبق اسم نوع الاستثناء UserAccountException الجديد، هو اسم الحِزم التي يتواجد بها في مشروع البرنامج. أمّا ما يأتي بعد النقطتين المتعامدتين (:)، فهو نص الرسالة المُخصصة التي وضعناها سابقًا.

إعادة تعريف overriding الخاصيّة message:

يمكننا استخدام الطريقة في الفقرة السابقة لإرفاق نص مُخصص مع رسالة الاستثناء. أو بما أن الصنف Throwable وهو الصنف الأب بالنسبة Exception يملك خاصيّة باسم message، وهو ما تظهره الصورة التالية:

شكل الصنف الأب لكل الأخطاء Errors والاستثناءات Exceptions الصنف Throwable في مكتبة كوتلن القياسية

يمكننا عمل override لها في صنفنا الجديد لأنه يرث من Throwable بطريقة غير مباشرة:

class UserAccountException(override val message: String): Exception()

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

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

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

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

كتلتي try-catch كتعبير Expression يعيد قيمة:

تعتبر الكتلتين تعبير يعيد قيمة في لغة كوتلن. وهذا يعني، أنه يمكننا وضع القيمة التي تنتجها إحدى الكتلتين، في متغير مثلًا:

fun main() {

    val age = readln()

    val checkInput = try {
        "Your age is: ${age.toInt()}"
    } catch (nfe: NumberFormatException) {
        "Please enter only numbers"
    }

    println(checkInput)
}

في الشفرة أعلاه، بعد قراءة مُدخلات المستخدم وتخزينها في المتغير age، نحاول في كتلة try، تحويل المُدخلات من نص String إلى عدد من النوع Int، عبر دالة ()toInt من مكتبة كوتلن القياسية. إذا لم تتمكن الدالة من تحويل النص إلى عدد، سيتم رمي الاستثناء NumberFormatException، لذلك نلتقطه في كتلة catch. 

ونسند ما تعيده الكتلتين، إلى المتغير checkInput. والقيمة التي سيتم إسنادها للمتغير، هي أمّا القيمة العائدة من try، أو القيمة العائدة من catch. لذا عند إدخال العدد 8 مثلًا لهذا البرنامج، ستكون نتيجة طباعة المتغير checkInput هي:

Your age is: 8

وهي القيمة العائدة من try. لأن الدالة لم تجد مشكلة في تحويل النص “8” إلى العدد 8، تم تنفيذ كتلة try وتخزين قيمتها في المتغير. أمّا إذا أدخلنا النص “Eight” مثلًا، هنا لن تستطيع الدالة تحويل النص إلى عدد، وسيحدث استثناء NumberFormatException. بالتالي، سيتم تنفيذ كتلة catch، والقيمة التي تعيدها هذه الكتلة، هي التي سيتم تخزينها في المتغير. لذا ستكون نتيجة طباعة المتغير هي:

Please enter only numbers

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

كتلة finally:

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

finally مع كتلة try فقط:

عند حدوث استثناء في كتلة try، ستتم معالجته في كتلة catch، بالتالي، سيتم تنفيذ الشفرة التي تلي حدوث الاستثناء لأنه تم تجنب تحطم البرنامج. أمّا إذا لم تكون هناك كتلة catch، أو بمعنى آخر لم يكن من الممكن تلافي تحطم البرنامج، حينها لن يتم تنفيذ أي شيء يأتي بعد سطر حدوث الاستثناء، ما عدا كتلة finally:

fun main() {

    try {

        throw Exception("Exception happened")

    } finally {
        println("from finally block")
    }

    println("Some code")
}

في الشفرة أعلاه، لدينا كتلتي try و finally فقط. عند حدوث استثناء في كتلة try – وهو ما سيحدث لأننا نرمي استثناء عبر throw داخل الكتلة -، ليس هناك كتلة catch لمعالجته. لذا عند تنفيذ الشفرة، ستكون النتيجة هي:

سيتم تنفيذ الشفرة في كتلة finally حتى لو تحطم البرنامج بفعل استثناء

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

finally مع كتلتي try و catch:

وعند وجود catch سيتم تجنب تحطم البرنامج وتنفيذ الأسطر التالية. وبالطبع، finally، سيتم تنفيذها كالعادة:

fun main() {

    try {

        throw Exception("Exception happened")

    }catch (e: Exception) {
        println("Exception handled")
    } finally {
        println("from finally block")
    }

    println("Some code")
}

ستكون نتيجة التنفيذ هي:

Exception handled
from finally block
Some code

الخلاصة:

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

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

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

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