تعلّم البرمجة بلغة كوتلن (70): الأصناف الداخلية Inner Classes

تعلّم البرمجة بلغة كوتلن (70): الأصناف الداخلية Inner Classes
أستمع الى المقال

حينما نضع صنف أو حتى أصناف عديدة داخل صنفٍ ما، تصبح هذه الأصناف (هي وكل مكوناتها من خاصيّات ودوال …الخ) أعضاء Members تتبع لهذا الصنف الذي يعرف بالصنف الخارجي Outer Class، كما شرحنا في درس الأصناف المُتداخِلة Nested Classes. ولكن رغم ذلك، لا يوجد رابط حقيقي بين هذه الأصناف العضوة والصنف الخارجي. ولربط هذه الأصناف المُتداخلة بالصنف الخارجي، يجب أن نحولها إلى أصناف داخلية Inner Classes.

ما هي الأصناف الداخلية Inner Classes:

هي أصناف تكتب مثل الأصناف المُتداخِلة داخل صنف آخر. الفرق سيكون، أن الأصناف الداخلية، تسبقها الكلمة المفتاحية inner. بمجرد وضع هذه الكلمة قبل الكلمة المفتاحية class واسم الصنف نفسه، يكون هناك رابط بين الصنف الداخلي والصنف الخارجي. ولفهم ذلك، يمكننا إلقاء نظرة على الشفرة التالية والتي كتبناها في  درس الأصناف المُتداخِلة:

class Airport(private val code: String) {
    class Plane {
        fun contact(airport: Airport) = "Contacting ${airport.code}"
    }
}

نلاحظ أن دالة ()contact الخاصة بالصنف المُتداخِل Plane، استطاعت الوصول إلى الخاصيّة code عبر استخدام كائن من الصنف الخارجي Airport. وسيحدث خطأ ولن يتم التعرف على الخاصيّة داخل الدالة بدون هذا الكائن. أي نعم استطاعت الدالة الوصول إلى الخاصيّة على الرغم من أن الوصول إليها محدد بـ private، ولكن هذا حدث لأن الصنف Plane (مع مكوناته مثل الدالة ()contact)، هو عضو في الصنف الخارجي Airport.

ولهذا، عند استدعاء دالة ()contact، يجب أن نرسل إليها كائن من النوع Airport. هذا ليس رابط حقيقي بين الصنف المتداخل والخارجي. الرابط الحقيقي سيكون، حينما يستطيع الصنف المتداخل (العضو)، استخدام الخاصيّة، حتى بدون إنشاء كائن من الصنف الخارجي. لفعل ذلك، يمكننا بكل بساطة إضافة الكلمة inner قبل الكلمة class للصنف Plane:

class Airport(private val code: String) {
    inner class Plane {
        fun contact() = "Contacting $code"
    }
}

بعد إضافة الكلمة inner، تستطيع الآن دالة ()contact الوصول إلى الخاصيّة code حتى بدون إنشاء كائن من الصنف الخارجي. وهذا لأن الصنف Plane أصبح مرتبط بالصنف Airport. وكنتيجة لهذا الربط، يمكن لكل مكونات الصنف Plane الوصول لكل مكونات الصنف الخارجي مباشرةً.

استخدام الصنف الداخلي:

خلافًا للأصناف المُتداخِلة، يمكن الوصول للصنف الداخلي وإنشاء كائن منه، فقط عبر إنشاء كائن من الصنف الخارجي أولاً. لأن الصنف الداخلي، هو جزء من الصنف الخارجي، ولا يتواجد بداخله من أجل عملية تنظيم الشفرة وحسب. فمثلًا، إذا أردنا استخدام الصنف Plane في الدالة الرئيسية ()main، فسنفعل التالي:

fun main() {
   val plane = Airport("CAI").Plane()
}

السطر يمين علامة الإسناد =، هو الطريقة المختصرة في كوتلن لإنشاء الكائنين في سطر واحد. ويمكن بالطبع إنشاء الكائنين منفصلين كالتالي:

fun main() {
   val airport = Airport("CAI")
   val plane = airport.Plane()
}

أي الطريقتين استخدمنا، سيكون نوع بيانات المرجع reference (أو المتغير) المسمى plane، هو:

Airport.Plane

وهذا يعني أن نوع بياناته هو النوع Plane، ولكن لتواجد الصنف داخل صنف آخر، يجب الإشارة إليه بهذه الطريقة. بعد إنشاء الكائن من الصنف الداخلي، يمكننا أن نصل إلى أعضائه، مثل دالة ()contact وطباعة النتيجة العائدة منها:

fun main() {
   val airport = Airport("CAI")
   val plane = airport.Plane()

   println(
        plane.contact()
    )
}

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

Contacting CAI

وهو النص الذي تنتجه دالة ()contact.

الكلمة المفتاحية this:

عند استخدام الأصناف في برنامجنا، يمكننا أن نشير إلى الصنف أو الكائن الحالي، عبر الكلمة المفتاحية this. وهي كلمة شرحناها باستفاضة عند ورود إشارة إليها، في أكثر من 10 دروس في هذه الدورة، بدءًا من الدرس 38. ولأن this تُمثل المرجع للكائن الحالي، لذا يمكننا استخدامها في استدعاء أعضائه من خاصيّات ودوال.

وعند استخدام this مع الأصناف العادية البسيطة، يكون الأمر واضحًا إلى أي صنف تشير. ولكن كيف يمكننا استخدامها داخل صنف يحتوي على عدة أصناف داخلية؟ إلى أي صنف ستشير في هذه الحالة؟ لحل هذه المشكلة، قدمت لغة كوتلن جملة برمجية تعرف بـ qualified this syntax. وهي تتكون من الكلمة this تتبعها العلامة @ ثم اسم الصنف المُراد:

this@className

كمثال، إذا كان لدينا صنف يُمثل الكائن حاسب Computer. داخل هذا الصنف لدينا صنف داخلي يُمثل ذاكرة التخزين Disk. وداخل صنف الذاكرة نفسه، لدينا صنف داخلي يُمثل نظام التشغيل OS. وداخله أيضًا لدينا صنف داخلي آخر يُمثل البرنامج Program:

class Computer(val diskSize: Int) {

    inner class Disk(private val osSize: Int) {
        fun download(): Int {
            return this@Computer.diskSize - this.osSize
        }

        inner class OS(private val programSize: Int) {
            fun download(): Int {
                return this@Disk.download() - this.programSize
            }

            inner class Program(private val fileSize: Int) {
                fun download(): Int {
                    return this@OS.download() - this.fileSize
                }
            }
        }
    }
}

نلاحظ أن الأصناف الداخلية الثلاث، لديها دوال تحمل نفس الاسم download ونفس التوقيع. لا وجود لمفهوم الـ overriding بين هذه الدوال الثلاث، لعدم وجود علاقة وراثية بين هذه الأصناف. فإذا استدعينا دالة ()download في الصنف الداخلي عبر استخدام الكلمة this فقط، سيتم استدعاء الدالة الخاصة بالصنف الذي يجري فيه الاستدعاء. لاستدعاء دالة من صنف داخلي أو خارجي آخر، يجب أن نستخدم طريقة الـ qualified this syntax المذكورة أعلاه.

لذلك، في دالة ()download الخاصة بالصنف Disk، نستخدم نفس الطريقة لاستدعاء الخاصيّة الخاصة بالصنف الخارجي Computer. أمّا الدوال والخاصيّات الخاصة بالصنف نفسه، يمكننا استدعاؤها عبر this فقط أو يمكننا التخلي عنها.

ولفهم طريقة عمل الشفرة والنتيجة التي تنتجها، يمكننا إنشاء كائنات من هذه الأصناف في دالة ()main، وطباعة القيم العائدة من الدوال الثلاث:

fun main() {

    val computer = Computer(100)
    println("Computer object created with disk size ${computer.diskSize} GB")

    var currentDiskSize: Int

    val disk = computer.Disk(4)
    currentDiskSize = disk.download()
    println("Operating system downloaded. free disk space: $currentDiskSize")

    val os = disk.OS(3)
    currentDiskSize = os.download()
    println("Program downloaded. free disk space: $currentDiskSize")

    val pro = os.Program(2)
    currentDiskSize = pro.download()
    println("File downloaded. free disk space: $currentDiskSize")

    println("Current free disk space: $currentDiskSize")
}

وعند تنفيذ الشفرة، ستكون النتيجة هي:

Computer object created with disk size 100 GB
Operating system downloaded. free disk space: 96
1 program downloaded. free disk space: 93
1 file downloaded. free disk space: 91
Current free disk space: 91

أصناف داخلية محلية ومجهولة:

الأصناف التي يتم كتابتها داخل الدوال، تعرف أيضًا بالأصناف الداخلية المحلية local inner classes. يمكن كتابة هذه الأصناف بالطريقة العادية، أو إنشاؤها بشكل مجهول anonymously، عبر استخدام تعبير كائن object expression، أو باستخدام تحويلات SAM. في كل الحالات، لا يتم استخدام الكلمة المفتاحية inner، ولكنها تكون ضمنية.

فمثلًا، إذا كان لدينا واجهة الدالة الواحدة Fun Interface التالية:

fun interface Pet {
    fun speak(): String
}

واجهة الدالة التي أسميناها Pet، أنشأناها لتُمثل كائن حيوان أليف Pet، وبها دالة واحدة فقط تعيد نتيجة من النوع String. فكما شرحنا في درس الواجهات Interfaces، أنه يمكننا تنفيذ هذه الواجهة عبر تطبيقها من قبل الأصناف العادية، أو باستخدام تحويلات SAM، عبر تمرير تعبير لامبدا إليها. وطريقة أخرى يمكننا استخدامها وهي عبر استخدام صنف مجهول، والتي سنستعرضها في الفقرات التالية.

طريقة الأصناف المحلية العادية:

يمكن تنفيذ واجهة الدالة عبر صنف عادي كالتالي:

object CreatePet {

    fun dog(): Pet {
        class Dog : Pet {
            override fun speak() = "Bark"
        }
        return Dog()
    }

}

لدينا صنف CreatePet والذي استخدمنا معه النمط المُفرد Singleton عبر الكلمة object، لأننا نحتاج منه كائن واحد فقط، طوال فترة تشغيل البرنامج. داخل هذا الصنف، لدينا دالة ()dog، والتي تعيد قيمة من النوع Pet. داخل الدالة لدينا صنف Dog وهو صنف داخلي محلي (هو صنف داخلي رغم عدم وجود كلمة inner لأنه يتم تضمينها تلقائيًا). ولأن الصنف Dog يطبق واجهة الدالة Pet، كان لزامًا عليه أن يطبق الدالة التي تحتويها الواجهة. وفي آخر سطر في الدالة، نعيد كائن من الصنف Dog لأن الدالة تحتاج أن تعيد كائن من النوع Pet أو من الأصناف التي تتفرع منه، تطبيقًا لمفهوم تحويل النوع لأعلى upcasting.

للوصول للدالة داخل الصنف Dog وطباعة نتيجتها، لابد من استخدام كائن منه. وللوصول للصنف نفسه، يجب أن نصل إلى الدالة التي تحتويه وهي دالة ()dog. وللوصول للدالة، يجب أن نستخدم الكائن CreatePet والذي تتواجد بداخله، كالتالي:

CreatePet.dog().speak()

وعند إرسال السطر أعلاه إلى دالة الطباعة داخل الدالة الرئيسية ()main:

fun main() {
    println(CreatePet.dog().speak())
}

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

Bark

وهو النص الذي تعيده دالة ()speak والتي تم تطبيقها من قبل الصنف Dog. هكذا نكون قد طبّقنا واجهة الدالة، عبر صنف محلي داخلي عادي.

طريقة الصنف المجهول:

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

فلنفترض أنه لدينا حيوان أليف آخر وليكن ببغاء parrot وأضفنا دالة خاصة به:

object CreatePet {
    fun dog(): Pet {
        // Local inner class:
        class Dog : Pet {
            override fun speak() = "Bark"
        }
        return Dog()
    }

fun parrot(): Pet {
        // Anonymous inner class:
        return object : Pet {
            override fun speak() = "talk"
        }
    }

}

تعيد دالة ()parrot قيمة من النوع Pet أيضًا. نلاحظ أن الشفرة أصبحت أقل وأكثر قابلية للقراءة من الشفرة في الفقرة السابقة. وسيكون استدعاء دالة ()speak، بنفس الطريقة أعلاه:

fun main() {
    println(CreatePet.parrot().speak())
}

لتكون نتيجة الطباعة هي:

talk

وهي القيمة التي تعيدها دالة ()speak التي تم تطبيقها داخل الصنف المجهول في دالة ()parrot. طريقة أخرى أكثر اختصارًا من الصنف المجهول، وهي تطبيق واجهة الدالة، عبر استخدام مفهوم تحويلات SAM.

طريقة تحويلات SAM:

هي طريقة تمكننا من ارسال القيمة التي تعيدها الدالة المتواجدة في واجهة الدالة، عبر إرسالها في تعبير لامبدا. ولنفترض أنه لدينا دالة تعيد كائن حيوان أليف من نوع آخر، وليكن قط Cat:

object CreatePet {
    fun dog(): Pet {
        // Local inner class:
        class Dog : Pet {
            override fun speak() = "Bark"
        }
        return Dog()
    }

fun parrot(): Pet {
        // Anonymous inner class:
        return object : Pet {
            override fun speak() = "talk"
        }
    }

    fun cat(): Pet {
        // SAM conversion:
        return Pet { "Meow" }
    }
}

دالة ()cat هي الأخرى تعيد قيمة من النوع Pet. ولكن هذه المرة، نرسل القيمة المُراد طباعتها، عبر استخدام تعبير لامبدا. أمّا طريقة استدعاء الدالة ()speak تتم بنفس الطريقة السابقة:

fun main() {
    println(CreatePet.cat().speak())
}

لتكون نتيجة الطباعة هي:

Meow

الخلاصة:

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

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

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