تعلّم البرمجة بلغة كوتلن (60): تحويل النوع للأعلى Upcasting

تعلّم البرمجة بلغة كوتلن (60): تحويل النوع للأعلى Upcasting
أستمع الى المقال

عندما يرث صنف من صنف آخر، يمكننا تسمية الصنف الأب الموروث منه بالنوع الأعلى Supertype والصنف الابن الوارث بالنوع الفرعي Subtype. هذه التسميات تأتي مما رأيناه في درس الوراثة، بأن الصنف الأب يكون في أعلى قمة التسلسل الهرمي الذي يصف العلاقات الوراثية بين الأصناف.

يمكننا استغلال هذه العلاقات الوراثية، في استخدام الصنف الأعلى، للإشارة إلى الأصناف الفرعية التي تحته. مما يحقق أحد أهم مبادئ الوراثة، وهو إعادة استخدام نفس الشفرة داخل الشجرة الوراثية. هذه الآلية تعرف بـ تعدد الأشكال Polymorphism، والتي سنشرحها في الدرس القادم. أمّا في هذا الدرس، وكتمهيد لفهم تعدد الأشكال، سنقوم بدراسة مفهوم تحويل النوع لأعلى Upcasting.

ما هو تحويل النوع لأعلى Upcasting:

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

interface Shape {
    fun draw(): String
    fun erase(): String
}

class Circle : Shape {
    override fun draw() = "Drawing the Circle"
    override fun erase() = "Erasing the Circle"
}

class Square : Shape {
    override fun draw() = "Drawing the Square"
    override fun erase() = "Drawing the Square"
    fun color() = "Square color"
}

لدينا في الشفرة، واجهة Interface بها دالتان مجرّدتان abstract هما: ()draw و ()erase. ثم لدينا صنفان: Circle و Square يطبّقان هذه الواجهة. وبما أنهما يطبقان الواجهة، كان عليهما أن يطبقا الدوال المجرّدة المتواجدة في الواجهة أيضًا، كما شرحنا في درس الواجهة. وبالطبع يمكن أن يمتلك الصنفان، دوال خاصة بهما غير المتواجدة في الواجهة، كما يظهر في الصنف Square الذي يمتلك الدالة ()color.

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

fun main() {

    fun show(shape: Shape) {
        println(shape.draw())
    }

    show(Circle())
    show(Square())
}

داخل دالة ()main، لدينا دالة أخرى وهي ()show. لدى دالة ()show معامل واحد من النوع الأعلى للصنفين Circle و Square وهو الواجهة Shape. داخل الدالة نطبع عبر دالة الطباعة، القيمة التي تعيدها دالة ()draw والتي نستدعيها عبر مرجع أو كائن من النوع الأعلى.

ولكن عند استدعاء الدالة مرتين في آخر دالة ()main، أرسلنا لمعاملها قيم كائنين مختلفين عن نوع معاملها. ورغم ذلك، سنجد أنه يتم استدعاء دالة ()draw الخاصة بكل كائن في لحظة الاستدعاء. لتكون نتيجة التنفيذ هي:

Drawing the Circle
Drawing the Square

وهنا يظهر أنه حدث تحويل للأنواع الفرعية إلى النوع الأعلى، قبل أن يتم استدعاء دالة ()draw. وهذا ما يعرف بالـ upcasting.

قابلية الاستبدال Substitutability:

إن معاملة نوع معين كنوع أكثر عمومية (نوع أعلى) هي النقطة الأساسية في عملية الوراثة ككل. تتواجد آلية الوراثة لتحقيق هدف تحويل النوع الفرعي إلى النوع الأعلى. وكما رأينا في المثال، أن ممارسة التجريد (“كل شيء هو عبارة عن Shape”)، تمكننا من كتابة دالة ()show واحدة بدلاً من كتابة واحدة لكل نوع من الأصناف.إذًا، الـ Upcasting هو وسيلة لإعادة استخدام التعليمات البرمجية للكائنات.

وإذا كان الهدف الأساسي في الوراثة هو القدرة على استبدال نوع فرعي مشتق لنوع أعلى، فماذا سيحدث للدوال الأعضاء الإضافية، مثل دالة ()color في الصنف Square؟ قابلية الاستبدال، وتسمى أيضًا مبدأ قابلية استبدال Liskov، تنص على أنه بعد التحويل لأعلى، يمكن معاملة النوع الفرعي المشتق تمامًا مثل النوع الأعلى، لا أكثر ولا أقل.

هذا يعني أن أي دوال عضو مضافة إلى النوع الفرعي، تم التخلي عنها. نعم هي لاتزال موجودة في الصنف، ولكن نظرًا لأنها غير متواجدة في النوع الأعلى، بالتالي لن تكون متوفرة داخل دالة ()show، حيث يجري التحويل للأعلى للأنواع.

لذلك، استدعاء هذه الدالة كالتالي:

fun show(shape: Shape) {
        println(shape.draw())
        println(shape.color())
 }

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

Unresolved reference: color

لم يتعرف المترجم على دالة ()color على الرغم من أنها متواجدة في نفس الصنف الذي تتواجد به دالة ()draw التي تم تنفيذها بالفعل. وهذا يعني، أن الدوال التي نستطيع استدعائها عند عمل تحويل للأعلى للأنواع، هي الدوال المشتركة والتي تتواجد في الصنف الأعلى.

إنشاء كائن فرعي من النوع الأعلى:

نفس الأمر سيحدث، عندما ننشئ كائن فرعي ونسنده إلى متغير من النوع الأعلى:

fun main() {

    val shape1: Shape = Square()
    val shape2: Shape = Circle()

    shape1.draw()
    shape1.erase()
    shape1.color
    
    shape2.draw()
    shape2.erase()
    
}

يمكننا إسناد كائنات من الأصناف Square و Circle إلى متغيرات من النوع Shape كما يظهر في المثال أعلاه. ولكن بما أن هذه عملية تحويل للأعلى upcasting، عندها أيضًا لن تتوفر الدالة color الخاصة بالصنف Square. وعند محاولة استدعاء الدالة لن يتعرف عليها المترجم كما يظهر في الصورة التالية من برنامج IntelliJ:

الخلاصة:

عند القيام بتحويل النوع لأعلى upcasting، ستتوفر الدوال المتواجدة في الصنف الأعلى فقط.

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

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