تعلّم البرمجة بلغة كوتلن (67): فحص النوع Type Checking

تعلّم البرمجة بلغة كوتلن (67): فحص النوع Type Checking
أستمع الى المقال

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

ما هو فحص النوع Type Checking:

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

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

التسلسل الهرمي للنوع حشرة:

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

لدينا في أعلى الصورة النوع حشرة Insect. ثم لدينا قسمين منه: الأول هو الحشرات الأساسية Basic Insect، وهو النوع الأساسي الأكثر عددًا في أنواع الحشرات، والثاني حشرات الماء Aquatic Insect، وهو الأقل عددًا.

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

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

تمثيل الكائن حشرة عبر الشفرة:

يمكننا تطبيق الشرح النظري أعلاه، عبر الشفرة كالآتي:

interface Insect {
    fun fly() = "${this::class.simpleName}: can fly"
    fun walk() = "${this::class.simpleName}: can walk"
}

interface BasicInsect: Insect

class Bee: BasicInsect

class Butterfly: BasicInsect

interface AquaticInsect : Insect {
    fun swim() = "${this::class.simpleName}: can swim"
    fun walkOnWater() = "${this::class.simpleName}: can walk on water"
}

class WaterBeetle : AquaticInsect

class WaterStrider : AquaticInsect

أولًا لدينا الواجهة الرئيسية Insect، والتي بها دالتان تُمثلان الوظائف الأساسية لنوع الحشرات الأكثر عددًا. ولدينا واجهة أخرى BasicInsect تطبّق الواجهة الرئيسية Insect، وهي في الوقت ذاته الواجهة الرئيسية للصنفين: Bee و Butterfly. وأخيرًا واجهة AquaticInsect التي تُمثل حشرات الماء، وبها دوال خاصة بهذا النوع من الحشرات، وهي في الوقت ذاته الواجهة الرئيسية للصنفين: WaterBeetle و WaterStrider.

الواجهتان BasicInsect و AquaticInsect، غير مجبرتان على تطبيق دالتي الواجهة الرئيسية Insect، لأن الدالتين: ()fly و ()walk غير مجرّدتين وتم تطبيقهما في الواجهة الرئيسية. وتستطيعان أيضًا إضافة دوال أخرى كما في واجهة AquaticInsect والتي بها دالتي: ()swim و ()walkOnWater وهما كما يظهر من اسميهما، تخص النوع حشرات الماء.

أمّا التعبير الذي يظهر في كل النصوص التي تعيدها الدوال:

this::class.simpleName

فهو تعبير سيعيد أسم الصنف الذي يستدعي الكائن المُنشأ منه الدالة. ويمكننا اختصاره كالتالي:

val Any.name
    get() = this::class.simpleName

نستخدم طريقة الخاصيّات الاضافية، ونضيف خاصيّة باسم name للنوع Any، وهو النوع الأعلى لكل الأنواع في لغة كوتلن. ثم باستخدام دالة ()get، نسند إليه القيمة التي سيُمثلها عند استخدامه مع أي كائن. كأننا نقول: حينما يستدعي أي Any كائن الخاصيّة name، ضع مكانها القيمة this::class.simpleName، وهو التعبير الذي سيعيد اسم الصنف الذي تم إنشاء الكائن منه.

لتكون الشفرة بعد إعادة كتابتها، كالتالي:

val Any.name
    get() = this::class.simpleName

interface Insect {
    fun fly() = "$name: can fly"
    fun walk() = "$name: can walk"
}
interface BasicInsect: Insect

class Bee: BasicInsect

class Butterfly: BasicInsect

interface AquaticInsect : Insect {
    fun swim() = "$name: can swim"
    fun walkOnWater() = "$name: can walk on water"
}

class WaterBeetle : AquaticInsect

class WaterStrider : AquaticInsect

بعد استبدال التعبير السابق بالخاصيّة name، تخلصنا من بعض الشفرات، وزادت قابلية القراءة. الآن أصبحت شفرة النوع حشرة، جاهزة لاستخدامها.

فحص النوع عبر استخدام الأنواع الرئيسية:

لدينا واجهات تُمثل أنواع مختلفة من الحشرات. إذًا كيف يمكننا استخدامها؟ سنستخدم الكلمة is لفحص كل نوع ومن ثم استدعاء الدالة التي تخصه:

fun Insect.basicInsect() =
    if (this is BasicInsect)
        "${walk()} - ${fly()}"
    else
        "$name: It's water insect"

أضفنا دالة مُلحقة بالواجهة Insect، وداخل الدالة نستخدم كتلة if، والتي تفحص ما إذا كان الكائن الذي يستدعي الدالة، هو من النوع BasicInsect. الكلمة this تُشير إلى كائن Insect. والتعبير this is BasicInsect، ستكون نتيجته true إذا كان هذا الكائن من النوع BasicInsect، وسيتم تنفيذ الدالتين: ()walk و ()fly. أمّا إذا كان الكائن من نوع آخر، من النوع AquaticInsect كمثال، سيتم إعادة النص: $name: It’s water insect مع استبدال name باسم الكائن.

استدعاء الدالة عبر الأصناف الفرعية:

نلاحظ أن الدالة المُلحقة ()basicInsect، تم إلحاقها بالنوع الرئيسي Insect. ثم في كتلة if، استطعنا استخدام عملية فحص النوع عبر استخدام النوع الفرعي BasicInsect. هذا لأن النوع BasicInsect هو Insect، لذا اخترنا علاقة الوراثة بين الواجهتين. والصنفان Bee أو Butterfly، يرثان BasicInsect لذا هما Insect بطريقة غير مباشرة، وتتوفر لهما الدوال من الواجهة Insect.

بالتالي، يمكننا استدعاء دالة ()basicInsect المُلحقة بالواجهة الرئيسية، عبر استخدام كائنات من الصنفين الفرعيين:

Bee().basicInsect()
Butterfly().basicInsect()

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

Bee: can walk - Bee: can fly
Butterfly: can walk - Butterfly: can fly

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

WaterStrider().basicInsect()
WaterBeetle().basicInsect()

ولأن كلا الكائنين: WaterStrider و WaterBeetle ليسا من النوع BasicInsect، ستعيد كتلة if النص التالي:

WaterBeetle: It's water insect
WaterStrider: It's water insect

فحص النوع عبر استخدام الأنواع الفرعية:

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

fun Insect.waterInsect() =
    when (this) {
        is WaterBeetle -> swim()
        is WaterStrider -> walkOnWater()
        else -> "$name: will drown in the water"
    }

داخل الدالة المُلحقة ()waterInsect، لدينا كتلة when والتي ستفحص this. الكلمة this هنا أيضًا تُمثل كائن الـ Insect الذي يستدعي هذه الدالة. داخل when، تجري عمليات لفحص نوع this. إذا كانت قيمة this هي كائن WaterBeetle ستنفذ الدالة ()swim. أمّا كانت القيمة هي WaterStrider ستنفذ الدالة ()walkOnWater. غير ذلك، ستعيد الجملة النصية في الشرط الفرعي else.

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

WaterBeetle: can swim
WaterStrider: can walk on water
Bee: will drown in the water
Butterfly: will drown in the water

كما نرى، نفذت كتلة when الدالتين اللتين تم الإعلان عنهما في واجهة AquaticInsect، عند استخدام كائني WaterBeetle و WaterStrider. ولكن على الرغم من أن كائني Bee و Butterfly هما كائنا Insect، لكنهما ليسا من النوع AquaticInsect. لذلك لا يمكنهما استدعاء الدالتين. وتعرفت كتلة when على كل كائن، وأنتجت النتيجة الصحيحة مع إبقاء الشفرة نظيفة وقابلة للقراءة، عبر ميزة فحص الأنواع والتحويل الذكي المتوفران في لغة كوتلن.

فحص قائمة من كائنات Insect:

عندما نضع أنواع مختلفة من كائنات Insect في قائمة List، سيتم فقط استدعاء الدوال التي يمكن لكل كائن أن يستدعيها:

fun main() {
    val insects = listOf(
        Bee(),
        WaterStrider(),
        WaterBeetle(),
        Butterfly()
    )

    println("Basic Insects:")
    insects.map { it.basicInsect() }.forEachIndexed { index, insect ->
        var i = index
        println("${++i}. $insect")
    }
}

نستدعي الدالة الوظيفية ()map على قائمة insects التي تحتوي على عدة كائنات Insect. الشرط في تعبير اللامبدا الذي نمرره للدالة، هو أن تعيد قائمة جديدة بعد تنفيذ دالة ()basicInsect على كائن في القائمة على حدى. ثم عبر دالة ()forEachIndexed، ندور على القائمة الجديدة التي تعيدها ()map، ونطبع عناصرها مرفقة بالفهرس Index. عند التنفيذ ستكون النتيجة:

Basic Insects:
1. Bee: can walk - Bee: can fly
2. WaterStrider: It's water insect
3. WaterBeetle: It's water insect
4. Butterfly: can walk - Butterfly: can fly

هذه هي نفس النتيجة السابقة، فقط هذه المرة استخدمنا قائمة وليس كائن واحد. نفس الأمر سيجري عند استخدام دالة ()waterInsect:

fun main() {
    val insects = listOf(
        Bee(),
        WaterStrider(),
        WaterBeetle(),
        Butterfly()
    )

    println("Water Insects:")
    insects.map { it.waterInsect() }.forEachIndexed { index, insect ->
        var i = index
        println("${++i}. $insect")
    }
}

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

Water Insects:
1. Bee: will drown in the water
2. WaterStrider: can walk on water
3. WaterBeetle: can swim
4. Butterfly: will drown in the water

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

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