تعلّم البرمجة بلغة كوتلن (68): الأصناف المُتداخِلة Nested Classes

تعلّم البرمجة بلغة كوتلن (68): الأصناف المُتداخِلة Nested Classes
أستمع الى المقال

تعرّفنا في دروس سابقة، على كيفية إنشاء الأصناف بالطريقة العادية، أو كأصناف مُجرّدة. في هذا الدرس، سنتعلم كيفية استخدام الأصناف بطريقة مختلفة، كأن نضع صنف أو أكثر، داخل صنف آخر، فيما يعرف بـ الأصناف المُتداخِلة Nested Classes.

الصنف المُتداخِل Nested Class:

الصنف المتداخل هو ببساطة كتابة صنف داخل المجال namespace الخاص بالصنف الخارجي Outer Class. المعنى الضمني هو أن الصنف الخارجي “يملك” الصنف المتداخل. لا يعد هذا الفعل أساسيًا في بناء البرنامج، ولكن يمكن أن يؤدي إلى جعل التعليمات البرمجية الخاصة بنا أكثر وضوحًا.

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

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

لدينا في الشفرة أعلاه، صنف خارجي Airport. وداخل المجال الخاص بهذا الصنف، لدينا صنف آخر اسمه Plane. الصنف Plane هنا، هو صنف متداخل Nested Class ويكون ظاهرًا فقط داخل مساحة الصنف الخارجي Airport. ولدى الصنف Plane أيضًا، دالة خاصة به وهي ()contact. وضعنا لدالة ()contact معامل واحد من نوع الصنف الخارجي Airport. وتعيد نص به تعبير يُمثل قيمة الخاصيّة الخاصة بالصنف الخارجي.

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

استخدام الصنف المُتداخِل:

لإنشاء كائن من الصنف المتداخل خارج الصنف الخارجي، يمكننا الوصول إليه عبر استخدام اسم الصنف الخارجي الذي يحتويه:

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

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

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

بنفس الطريقة، وجود الصنف المتداخل داخل صنف آخر، يحتم علينا الإشارة إلى رابط المجال الذي يتواجد به. أي الحزمة والملف (إذا كانا مختلفين) والصنف الخارجي الذي يتواجد به الصنف المتداخل. وللوصول للصنف Plane في دالة ()main، لم نحتاج لتضمين الحزمة واسم الملف، لأن الصنف Airport والدالة ()main، يتواجدان في نفس الحزمة والملف.

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

import oop.nestedClasses.Airport.Plane

بوضع هذا السطر أعلى الملف، نكون قد عرّفنا ملفنا الحالي على مكان وجود الصنف Plane. بالتالي، يمكننا استخدامه الآن في دالة ()main أو أي مكان آخر داخل هذا الملف، دون استخدام اسم الصنف الخارجي:

fun main() {
    val plane = Plane()
}

وبما أن المتغير plane يُمثل كائن من الصنف Plane، يمكننا عبر استخدام هذا المتغير الوصول إلى أعضاء الصنف Plane، مثل دالة ()contact:

import oop.nestedClasses.Airport.Plane
fun main() {
    val cairo = Airport("CAI")
    val plane = Plane()
    val contactAirport = plane.contact(cairo)

    println(contactAirport)
}

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

Contacting CAI

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

وضع محدد وصول خاص private للصنف المتداخل:

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

class Airport(private val code: String) {

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

    private class PrivatePlane : Plane()
}

الصنف PrivatePlane لن يظهر إلا داخل مجال الصنف الخارجي Airport لوجود المحدد private به. وعند محاولة استخدام الصنف PrivatePlane كما فعلنا مع الصنف Plane سابقًا:

val privatePlane = Airport.PrivatePlane()

سينتج الخطأ التالي:

Cannot access ‘PrivatePlane’: it is private in ‘Airport’

لايمكن الوصول إلى الصنف PrivatePlane لوجود محدد الوصول private. ما يمكننا فعله، هو إنشاء دالة داخل الصنف الخارجي Airport، وجعلها تعيد الصنف PrivatePlane، ثم القيام بعملية التحويل للأعلى upcast، إذا كان الصنف الأعلى لديه محدد وصول public:

class Airport(private val code: String) {

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

    private class PrivatePlane : Plane()

    fun privatePlaneFun(): Plane = PrivatePlane()
}

ما تقوم به دالة ()privatePlaneFun، هو إعادة كائن من الصنف PrivatePlane، ثم القيام بعملية تحويل النوع من الصنف المُشتق PrivatePlane، للصنف الأعلى Plane. وعند استدعاء دالة ()privatePlaneFun ستعيد كائن من النوع Plane:

fun main() {
    val frankfurt: Airport = 
        Airport("FRA")
    val plane: Plane = 
        frankfurt.privatePlaneFun()
    val contactAirport: String = 
        plane.contact(frankfurt)

    println(contactAirport)
}

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

Contacting FRA

وعلى الرغم من تمكننا من القيام بعملية تحويل نوع الصنف المُشتق للصنف الأعلى، ولكن لن نستطيع فعل العكس، أي عملية التحويل للأسفل downcast:

val privatePlane = plane as Airport.PrivatePlane

وسينتج خطأ لنفس السبب السابق، وهو أنه لا يمكن الإشارة أو استخدام الصنف PrivatePlane خارج مجال الصنف Airport، لأنه خاص private. فإذا حذفنا الكلمة private من الصنف PrivatePlane، عندها لن ينتج السطر السابق خطأ.

الأصناف المحلية Local Classes:

 الأصناف التي يتم الإعلان عنها داخل دالة Function، تعرف بالأصناف المحلية. فمثلًا، إذا كان لدينا الشفرة التالية:

fun localClasses() {
    open class Fruit
    class Apple : Fruit()
}

لدينا داخل دالة ()localClasses الصنف المفتوح للوراثة اسمه Fruit، وصنف آخر يرث منه اسمه Apple. كلا الصنفين يعتبران صنفين محليين، لتواجدهما داخل دالة. قد يبدو أنه من الأفضل جعل الصنف Fruit عبارة عن واجهة interface، ولكن لا يسمح بوضع واجهة داخل دالة.

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

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

interface Fruit

fun localClasses(): Fruit {
    class Apple: Fruit
    return Apple()
}

عدّلنا على الشفرة ونقلنا الصنف Fruit إلى خارج الدالة وغيرناه إلى interface. ثم جعلنا الدالة، تعيد كائن من النوع Fruit. داخل الدالة، مازال الصنف Apple يرث من Fruit. وفي آخر سطر جعلنا الدالة تعيد return، كائن من النوع ()Apple. بالطبع الدالة ستعيد كائن من النوع Fruit، لذلك ستجري عملية تحويل لأعلى، بتغيير نوع الصنف المُشتق Apple إلى نوعه الأعلى Fruit.

الآن يمكننا استدعاء الدالة ()localClasses في الدالة ()main:

fun main() {
    val fruit: Fruit = localClasses()
}

هنا أيضًا، لا يمكن استخدام عملية التحويل لأسفل downcast:

fun main() {
    val fruit: Fruit = localClasses()

    val f = fruit as Apple
}

لأن دالة ()localClasses تعيد كائن من النوع Fruit، بالتالي المتغير fruit يُمثل كائن من النوع Fruit. وبما أن الصنف Apple مُشتق من النوع Fruit، ففي الوضع الطبيعي سنتمكن من إجراء عملية التحويل من الصنف الأعلى Fruit إلى الصنف المُشتق Apple. ولكن لأن Apple هو صنف محلي داخل دالة، لا يمكن الوصول إليه في أي مكان آخر خارج الدالة التي يتواجد بها. لذلك، سطر التحويل لأسفل عبر الكلمة as، سينتج الخطأ:

Unresolved reference: Apple

لعدم معرفة المترجم أين يمكن إيجاد الصنف Apple.

الأصناف Classes داخل الواجهات interfaces:

يمكننا أن نُنشئ الأصناف داخل الواجهات. فلنفترض أنه لدينا واجهة تُمثل منتج Product:

interface Product {
    val productType: Type
    data class Type(val name: String)
}

داخل الواجهة، لدينا صنف بيانات باسم Type، وخاصيّة مُجرّدة اسمها productType والتي نوع بياناتها هو الصنف Type. الصنف Type، هو هنا الصنف المتداخل لتواجده داخل واجهة interface. لذا، للوصول إليه أو إنشاء كائن منه، يجب أن نستخدم اسم الواجهة Product ثم نقطة ثم اسم الصنف Type.

فإذا طبّقنا الواجهة على صنف آخر كالتالي:

class Mobile(type: String) : Product {
    override val productType = 
      Product.Type(type)
}

يجب على الصنف Mobile تطبيق override الخاصيّة productType. ولأن نوع بيانات الخاصيّة هو Type، يجب أن نسند إليها قيمة من نفس النوع عند تطبيقها. هذا يتطلب الوصول للصنف Type وإنشاء كائن منه ثم إسناده إلى الخاصيّة. وهو ما فعلناه عبر السطر التالي:

Product.Type(type)

وعند استخدام الشفرة أعلاه في دالة ()main كالتالي:

fun main() {
    val products = listOf(
        Mobile("Android"),
        Mobile("iPhone"),
        Mobile("Windows")
    )

    println(
        products.map(Product::productType)
    )
}

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

[Type(name=Android), Type(name=iPhone), Type(name=Windows)]

وهي قائمة List التي ستعيدها دالة ()map اعتمادًا على قيمة مرجع عضو الصنف، وهو هنا الخاصيّة productType.

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

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