تعلّم البرمجة بلغة كوتلن (57): الباني الثانوي Secondary Constructor

تعلّم البرمجة بلغة كوتلن (57): الباني الثانوي Secondary Constructor
أستمع الى المقال

شرحنا في درس سابق، ما هو الباني Constructor، وكيفية استخدامه في بناء كائن من الصنف. ولكن في بعض الأحيان، قد نحتاج إلى إنشاء عدة نسخ كائنات من نفس الصنف. ولأنه لا يمكن فعل ذلك عبر الباني الأساسي Primary Constructor وحده، سنحتاج إلى الإعلان عن بواني مخصصة Custom Constructors داخل الصنف، وتسمى في كوتلن بواني ثانوية Secondary Constructors. 

إنشاء الباني الثانوي المخصص:

يمكننا إنشاء باني ثانوي لصنف ما في وجود باني أساسي أو بدونه. ويكون في أبسط صوره، كتابة الكلمة الأساسية constructor داخل جسم الصنف، ثم أقواس ( ) المعاملات ثم جسم الباني الثانوي نفسه، والذي يبدأ وينتهي بالأقواس المعقوفة { }:

class Mobile {
    var company: String = "Unknown"
    var model: String = "Unknown"

    constructor(company: String, model: String) {
        this.company = company
        this.model = model
    }
}

لدينا صنف اسمه Mobile وبه خاصيّتان: company و model من النوع String. داخل الصنف لدينا باني ثانوي به معاملات تحمل نفس اسم الخاصيّات في الصنف. داخل هذا الباني المخصص، نسند القيم التي تأتي عبر معاملاته، إلى خاصيّات الصنف Mobile، والذي نُمثله بالكلمة this. كأننا نقول، الخاصيّات في this (هنا المقصود الصنف Mobile)، ستحصل على قيم معاملات الباني الثانوي هذا.

استبدال الكلمة this بالشرطة السفلية:

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

class Mobile {
    var company: String = "Unknown"
    var model: String = "Unknown"

    constructor(_company: String, _model: String) {
        company = _company
        model = _model
    }
}

هكذا سيكون من الواضح، أن: company_ و model_ هما معاملا الباني الثانوي، company و model المقصود منهما الخاصيّات في الصنف Mobile. لذا يكون واضحًا لنا وللمترجم أيضًا أن عملية الإسناد التالية، هي إسناد قيمة معاملات الباني الثانوي إلى خاصيّات الصنف:

company = _company
model = _model

إنشاء كائن من الصنف Mobile:

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

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

val mobile = Mobile("Samsung", "Galaxy S22")

ولأن كوتلن تخطت وضع باني افتراضي، لا يمكننا فعل التالي:

val mobile = Mobile()

لنتمكن من فعل ذلك، يجب أن يكون هناك باني ثانوي داخل الصنف Mobile ليس به معاملات:

class Mobile {
    var company: String = "Unknown"
    var model: String = "Unknown"

    constructor() {
        
    }
}

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

class Mobile() {
    var company: String = "Unknown"
    var model: String = "Unknown"
}

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

class Mobile {
    var company: String = "Unknown"
    var model: String = "Unknown"
}

عند استخدام أيًا من هذه الحالات الثلاث، نستطيع إنشاء كائن من الصنف Mobile كالتالي:

val mobile = Mobile()

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

إنشاء بواني متعددة في الصنف:

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

class Mobile {
    var company: String = "Unknown"
    var model: String = "Unknown"

    constructor()

    constructor(company: String, model: String) {

    }

    constructor(company: String, model: Int) {

    }

    constructor(company: String, model: String, year: Int) {

    }
}

يحتوي الصنف Mobile على بواني ذات تواقيع مختلفة. الأول ليس به معاملات (حذفنا الأقواس المعقوفة لأنه يمكننا فعل ذلك في كوتلن إذا كانت خالية ولا تحتوي على شفرة). الثاني به معاملين من النوع String. الباني الثالث به معاملين أيضًا ولكن يختلف عن السابق لأن المعامل الثاني هو من النوع Int. أمّا الباني الرابع والأخير، فهو يختلف بوجود معامل ثالث به من النوع Int.

الآن يمكننا إنشاء كائن من الصنف Mobile بأربعة طرق مختلفة:

fun main() {

    val mobile = Mobile()
    val mobile1 = Mobile("Samsung", "Galaxy S22")
    val mobile2 = Mobile("Samsung", 22)
    val mobile3 = Mobile("Samsung", "Galaxy S22", 2022)

}

نلاحظ أننا لم نغير أسماء المعاملات: company و model. وهذا لأن مترجم كوتلن لن يأخذها في الاعتبار عند التفريق بين البواني في الصنف الواحد. فما يهمه أكثر هو اختلاف أو ترتيب أنواع بيانات المعاملات أو عدد المعاملات.

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

constructor(company: String, model: String) {

}
constructor(company: String, year: String) {

}

حذف القيم الافتراضية للخاصيّات:

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

فمثلًا، يمكننا وضع الخاصيّات بدون قيم في الباني الأساسي كالتالي:

class Mobile(val company: String, val model: String)

وفي حالة الباني الثانوي، يجب أن لا نضع كلمتي val أو var قبل المعاملات لأنها ليست خاصيّات كما في الباني الأساسي. الباني الأساسي يمكنه استقبال معاملات وإسنادها لخاصيّات في أقواسه في الوقت ذاته ضمنيًا. أمّا الباني الثانوي، فهو لديه معاملات فقط، ويجب تهيئة أو إسناد القيم لخاصيّات الصنف صراحةً داخل أقواسه المعقوفة:

class Mobile {
    var company: String
    var model: String

    constructor(company: String, model: String) {
        this.company = company
        this.model = model
    }
}

نلاحظ أننا حذفنا النص “Unknown” الذي كنا نضعه كقيمة افتراضية للخاصيّتين. أمّا في حالة عدم وضع الخاصيّات في الباني الأساسي، أو عدم إسناد قيم لها في الباني الثانوي يجب أن نضع لها قيم افتراضية.

تفويض الباني Constructor Delegation:

كما شرحنا في درس الباني، عند عدم وجود أي باني في صنفٍ ما، ستوفر كوتلن باني افتراضي له، لنتمكن من إنشاء كائن منه. لذلك في حالة وجود باني ثانوي في الصنف، هذا يعني عدم وجود باني افتراضي فيه.

ولكن يمكننا بالطبع أن نضع له باني أساسي بأنفسنا. وعند وجود باني أساسي وباني ثانوي أو عدة بواني في نفس الصنف، يجب على هذه البواني الثانوية استدعاء الباني الأساسي. سواء مباشرةً عبر الكلمة this، أو بطريقة غير مباشرة باستدعاء باني ثانوي آخر والذي بدوره يستدعي الباني الأساسي. وهذا ما يعرف بـ التفويض Delegation.

فمثلًا، الشفرة التالية:

class Mobile(var company: String, var model: String) {
    
    constructor(company: String, model: String, year: Int) {

    }
}

وبالرغم من اختلاف توقيعات (اختلاف في أنواع بيانات المعاملات) الباني الأساسي والثانوي، ستنتج الشفرة أعلاه خطأ. وحل الخطأ يكون بتفويض الباني الثانوي إلى الباني الأساسي باستخدام الكلمة this، وإرسال قيم معاملات الباني الثانوي إلى معاملات الأساسي:

class Mobile(var company: String, var model: String) {

    constructor(company: String, model: String, year: Int) : this(company, model) {

    }
}

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

وهذا يعني أنه عند إنشاء كائن باستخدام الباني الثانوي:

val mobile = Mobile("Samsung", "Galaxy S22", 2022)

سيتم تنفيذ التعبير التالي أولًا:

this(company, model)

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

ترتيب تنفيذ شفرة الصنف:

عند إنشاء كائن من صنفٍ ما، سيعمل مترجم كوتلن على تنفيذ الشفرة داخل الصنف بترتيب معين. كمثال:

class Mobile(private val company: String, private val model: String) {

    init {
        println("$company best phone is $model")
    }

    constructor(company: String, model: String, year: Int) : this(company, model) {
        println("In $year $company best phone is $model")
    }

    init {
        println("Init block 2")
    }
}

إذا تم إنشاء الكائن عبر الباني الثانوي، وفي حالة وجود باني أساسي أيضًا، سيتم إسناد قيم لمعاملات الباني الأساسي أولًا قبل تنفيذ شفرة الباني الثانوي. وليحدث ذلك، سيجبرنا المترجم على تفويض الباني الثانوي إلى الباني الأساسي، عبر النقطتين المتعامدتين (:) والكلمة this، ثم فتح قوس ووضع قيم المعاملات.

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

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

لذا عند إنشاء كائن من الصنف Mobile عبر الباني الثانوي هذه المرة، سيتم تنفيذ دوال الطباعة في الصنف بالترتيب التالي:

Samsung best phone is Galaxy S22
Init block 2
In 2022 Samsung best phone is Galaxy S22

تم تهيئة الخاصيّات في الباني الأساسي بالقيم التي أتت عبر معاملات الباني الثانوي. لذلك لم تكن هناك مشكلة في استخدامها في جملة الطباعة في كتلة init الأولى. ثم تم طباعة الجملة في كتلة init الثانية. وأخيرًا تم طباعة الجملة المتواجدة في أقواس الباني الثانوي المعقوفة.

الخلاصة:

استعرضنا في هذا الدرس الباني الثانوي وكيفية استخدامه وتنفيذه وتفويضه إلى الباني الأساسي. في الدروس القادمة سنستعرض المزيد عنه عند استخدامه وعن التفويض عمومًا. ولكن قبل ذلك دعونا نلخص أهم النقاط التي وردت في الدرس:

  • بالإضافة للباني الأساسي أو الباني الافتراضي، يمكن للصنف أن يحتوي على بواني ثانوية.
  • ولإنشاء باني ثانوي مخصص في الصنف، يجب أن نسبقه بالكلمة المفتاحية constructor صراحةً.
  • إذا كان لدينا أكثر من باني ثانوي، يجب أن تحمل تواقيع (المعاملات) مختلفة عن بعضها البعض وعن الباني الأساسي إذا وجد.
  • نستطيع عدم وضع قيم افتراضية للخاصيّات في الصنف، إذا أسندنا لها قيم في الباني الثانوي.
  • يمكن الوصول إلى كل أعضاء الصنف داخله، باستخدام الكلمة المفتاحية this والنقطة (.).
  • عند وجود باني أساسي وثانوي، يجب على الثانوي أن يستدعي الباني الأساسي عبر الكلمة this، ويرسل له قيم لخاصيّاته – إن وجدت – لتهيئتها.
  • عند إنشاء كائن من أي صنف به باني أساسي وثانوي وكتل تهيئة init، أو ما سيتم تنفيذه هو تهيئة خاصيّة الباني الأساسي، ثم كتل init، وأخيرًا شفرة الباني الثانوي.

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

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