تعلّم البرمجة بلغة كوتلن (74): النوع لا شيء The Nothing Type

تعلّم البرمجة بلغة كوتلن (74): النوع لا شيء The Nothing Type
أستمع الى المقال

استعرضنا في دروس سابقة، العديد من الأنواع والأصناف في كوتلن. مثل: String و Int و Double و Unit وغيرها. كل هذه الأنواع والتي تأتي جاهزة في مكتبة كوتلن القياسية، بالإضافة للأصناف الخاصة بمشروعنا والتي نُنشئها بأنفسنا، كل هؤلاء يتم تنظيمهم في هيكل هرمي للأنواع. ويكون في أعلى هذا الهرم، النوع Any، وفي أسفله النوع Nothing. والنوع Nothing، هو موضوع درسنا اليوم.

ولكن قبل ذلك دعونا نفهم، ما هو التسلسل الهرمي للأصناف، كتمهيد لفهم النوع Nothing.

التسلسل الهرمي للأصناف في كوتلن:

الصورة التالية تُمثل نموذج لطريقة تسلسل العلاقات بين الأصناف في كوتلن:

كما نرى في الصورة، تنتمي كل الأصناف إلى الصنف Any، بما فيها الصنف الخاص بنا MyClass. أو يمكن أن تنتمي له بطريقة غير مباشرة، كما في صنفي Int و Long واللذان ينتميان إلى الصنف Number والذي بدوره ينتمي إلى الصنف Any.

وبما أن الصنف Any هو الصنف الأعلى SuperClass لكل هذه الأصناف، يمكننا أن نسند إلى متغير من النوع Any، كائن من أي نوع من الأصناف التي تأتي تحته. كمثال، يمكن أن نسند إليه كائن من النوع String:

val anyObject: Any = "This is string object"

كائن من النوع Int:

val anyObject: Any = 74

أو صنف خاص:

val anyObject: Any = MyClass()

أو الصنف Unit:

val anyObject: Any = Unit

(نلاحظ أننا لا نستدعي الباني الخاص بالصنف Unit عبر الأقواس ( )، لإنشاء كائن منه. وهذا لأن الصنف Unit هو كائن مُفرد من النوع object، وسيأتي شرحه لاحقًا في هذا الدرس).

إسناد كائن Nothing لمتغير من النوع Any:

ولكن، على الرغم من أن الصنف Nothing ينتمي هو الآخر للصنف Any كبقية الأصناف، ولكن لا يمكننا إسناد قيمة من النوع Nothing لمتغير من النوع Any مباشرةً. هذا لأن باني الصنف Nothing، تم تحديد الوصول إليه عبر private. بالتالي، لا يمكن استدعاؤه وإنشاء كائن منه، كالتالي:

val anyObject: Any = Nothing()

وبمجرد محاولة ذلك، كما في السطر أعلاه، سيظهر خط أحمر تحت كلمة Nothing في برنامج IntelliJ، للتعبير عن وجود خطأ سيمنع البرنامج عن التجميع. وعند وضع مؤشر الفأرة على الكلمة، ستظهر الرسالة التوضيحية التالية:

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

private constructor Nothing()

نلاحظ وجود private قبل الباني الرئيسي والذي تم ذكر كلمته المفتاحية constructor صراحةً. وبعد هذا السطر، يوجد شرح للنوع Nothing:

لا يمكن إنشاء كائنات (أو نسخ Instances) من النوع Nothing. يمكننا فقط استخدامه، لتمثيل “قيمة غير موجودة أبدًا”. كمثال، إذا كانت الدالة تحتوي على نوع الإرجاع ReturnType من النوع Nothing، فهذا يعني أنها لا تُرجع أي شيء مطلقًا (بل ترمي استثناءً دائمًا). 

إذًا، ما هو الهدف من وجود النوع Nothing في كوتلن؟ وكيف ومتى يمكننا استخدامه؟ سنجيب على ذلك نظريًا وعمليًا، في الفقرات أدناه.

كل الدوال – تقريبًا – تعيد قيمة في كوتلن:

عندما نُنشيء دالة، يمكننا أن نضع لها نوع الإرجاع ReturnType كالتالي:

fun function(): Int {
    return 74
}

دالة ()function لديها نوع إرجاع من نوع البيانات Int. لذا، يصبح لزامًا أن نعيد كائن يوافق نوع الإرجاع، في آخر سطر في الدالة. والعدد 74 يعتبر من النوع Int تلقائيًا، لأنه ليس عدد كسري ليصبح من النوع Double، وليس به حرف “L”، ليصبح من النوع Long.

وبالطبع، يمكننا اختصار كتابتها كالتالي:

fun function() = 74

هي فقط طريقة أخرى مختصرة لكتابة نفس الدالة وستنتج نفس النتيجة. دالة ()function، تعيد القيمة من النوع Int، والتي يمكننا استخدامها في مكان آخر داخل شفرة البرنامج، كطباعتها في دالة ()main مثلًا:

println(function())

وستكون نتيجة الطباعة هي: العدد 74، وهي القيمة التي تعيدها دالة ()function بعد إنتهاء عملها. ولكن، ماذا إذا أردنا القيام بطباعة القيمة، داخل دالة ()function نفسها، كالتالي:

fun function() = println(74)

في هذه الحالة، إذا وضعنا دالة ()function في دالة الطباعة في دالة ()main مرة أخرى، سيتم تنفيذ دالة الطباعة المتواجدة في دالة ()function أولًا، ثم طباعة القيمة التي تعيدها الدالة:

74
kotlin.Unit

العدد 74 هو ناتج الطباعة في دالة ()function. أمّا القيمة kotlin.Unit، فهي القيمة التي ستعيدها دالة ()function بعد إنتهاء عملها. وهي قيمة تُمثل النوع Unit، الذي يأتي مع اللغة، تمامًا مثله مثل باقي الأنواع: String و Int …الخ. 

وحتى لو كتبنا الدالة بالطريقة الأخرى:

fun function() {
    println(74)
}

نلاحظ أنه لم نذكر نوع الإرجاع في الدالة، ومع ذلك سيتم إعادة القيمة kotlin.Unit هذه المرة أيضًا. ويمكننا أيضًا كتابة اسم نوع الإرجاع:

fun function(): Unit {
    println(74)
}

هنا نضع النوع Unit، كنوع إرجاع لدالة ()function. ولكن هذا شيء زائد عن الحاجة، لأن أي دالة تقوم بعملٍ ما، ولا تعيد قيمة من نوع بيانات معين، ولكن لديها تأثير جانبي، مثل طباعة قيمة، ثم ما زال البرنامج يعمل بعد تنفيذ الدالة، هذه دالة تعيد النوع Unit تلقائيًا.

ما هو النوع Unit:

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

الصنف Unit هو كائن مُفرد Object:

تم اتباع نمط التصميم المُفرد Singleton، والذي يتم عبر الكلمة object في لغة كوتلن، في إنشاء الصنف Unit. وشكله في مكتبة كوتلن القياسية كالتالي:

public object Unit {
    override fun toString() = "kotlin.Unit"
}

داخل الكائن تم تطبيق دالة واحدة فقط وهي دالة ()toString المتواجدة في الصنف الأب Any. والقيمة “kotlin.Unit” التي تعيدها هذه الدالة، هي ما رأيناه سابقًا، عند طباعة قيمة دالة ()function.

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

val unitObject: Unit = Unit

وسيكون نوع المتغير هو Unit. أمّا عند طباعة هذا المتغير:

println(unitObject)

فستكون النتيجة هي: kotlin.Unit.

الصنف Nothing كنوع إرجاع:

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

fun infinite(): Nothing {
    while (true) { 
        println(1) 
    }
}

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

مثال عملي:

المثال أعلاه لا يبدو عمليًا، ولكنها إحدى طرق فهم ما هو الهدف من وجود النوع Nothing في لغة كوتلن. أمّا أهم مثال عملي يتم استخدم هذا النوع معه، هو دالة ترمي استثناء. دالة مثل هذه، لا تعيد شيئًا، وإنما توقف عمل البرنامج من الأساس. وكمثال من مكتبة كوتلن، لدينا دالة ()TODO. هذه الدالة، ترمي الاستثناء NotImplementedError. وشكل شفرتها هو كالتالي:

public inline fun TODO(): Nothing = throw NotImplementedError()

بعيدًا عن الكلمة inline والتي لم ندرسها بعد، نلاحظ أن الدالة لديها نوع إرجاع من النوع Nothing، وكل عملها هو رمي استثناء عبر الكلمة throw.

استخدام الدالة ()TODO:

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

كمثال، إذا كان لدينا الدالة التالية:

fun later(): Nothing = TODO()

لا نريد كتابة شفرة دالة ()later في هذا الوقت. لذا، وضعنا لها دالة ()TODO كقيمة إرجاع. ولأن دالة ()TODO لديها نوع إرجاع من النوع Nothing، توجب أيضًا أن نضع نوع الإرجاع Nothing لدالة ()later صراحةً.

ومتى ما استدعينا دالة ()later في مرحلة ما من مراحل التطوير، سيتم رمي الاستثناء:

إسناد Nothing لنوع آخر:

يجب أن نكتب نوع الإرجاع في دالة ()later، ولكن يمكن أن يكون نوع الإرجاع هذا، من أي نوع آخر في هرم الأنواع في كوتلن:

النوع Any:

fun later(): Any = TODO()

أو النوع Int:

fun later(): Int = TODO()

أو حتى النوع Unit:

fun later(): Unit = TODO()

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

val nothingVariable1: Nothing = 74
val nothingVariable2: Nothing = "Some text..."
fun nothingFunction() : Nothing = println("Something...")

كل الأسطر الثلاث، ستنتج خطأ. إذًا يجب إسناد قيمة من النوع Nothing، للمتغيرات والدوال التي يكون نوع بياناتها أو نوع إرجاعها النوع Nothing.

إسناد القيمة null:

عند إسناد القيمة null للمتغير، يضع له ترجم كوتلن النوع ?Nothing حتى ولم نكتب ذلك:

val nullVariable = null

أو يمكننا كتابته لمزيد من التوضيح:

val nullVariable: Nothing? = null

والنوع ?Nothing، هو النوع القابل للقيم الفارغة nullable من النوع Nothing. (شرحنا هذا في درس أنواع البيانات التي تقبل قيم فارغة).

وأيضًا عندما نُنشيء قائمة List عبر عنصر ابتدائي قيمته null، يكون نوع بيانات القائمة، هو النوع ?Nothing. سواء كتبناها هكذا:

val list = listOf(null)

أو هكذا:

val list: List<Nothing?> = listOf(null)

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

 val list: List<String?> = listOf(null)

الخلاصة:

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

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

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