تعلّم البرمجة بلغة كوتلن (43): مقدمة إلى البرمجة المُعمَمّة Generics

تعلّم البرمجة بلغة كوتلن (43): مقدمة إلى البرمجة المُعمَمّة Generics
أستمع الى المقال

درسنا في دروس سابقة، كيفية إنشاء كائن يحوي كائنات أخرى، فيما يعرف بـ التجميعات Collections في كوتلن. واستعرضنا ثلاث أنواع من هذه التجميعات: القوائم Lists والأطقم Sets والخرائط Maps.

فمثلًا، عندما نريد إنشاء قائمة لأي نوع بيانات أو حتى الأصناف، نستخدم الدالة ()listOf من مكتبة كوتلن، ونرسل لها العناصر التي يجب أن تحتويها القائمة:

فكما نرى، نعيد استخدام نفس الدالة لإنشاء قوائم مختلفة، لأنواع بيانات وأصناف مختلفة. فكيف حدث هذا؟ هذا حدث لأن دالة ()listOf كُتِبت وجُهزت لإستقبال أي نوع بيانات يتم إرساله إليها. وتعرف هذه الطريقة في البرمجة، بالبرمجة المُعمَمّة Generic Programming.

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

ما هي البرمجة المُعمَمّة:

يعني المصطلح “Generic” في البرمجة، “نوع عام، يناسب ويلائم مجموعة واسعة من الأنواع والأصناف”. تم استخدام أسلوب البرمجة المُعمَمّة في لغات البرمجة أساسًا، لإعطاء المبرمجين أكبر قدر من المرونة عند كتابة الأصناف أو الدوال، عن طريق إلغاء قيود تحديد نوع البيانات لها.

أحد أهم تطبيقاتها في كوتلن، هي التجميعات Collections كما قلنا أعلاه. ونحن أيضًا، يمكننا استخدامها، في إنشاء أصناف ودوال مُعمَمّة خاصة بنا، كما سنرى في الفقرات أدناه.

الأصناف المُعمَمّة Generic Classes:

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

شفرة بسيطة تتكون من الدالة ()main، وأربعة أصناف. الصنف Animal لتمثيل الحيوان في شفرتنا، أيًا كان نوع هذا الحيوان. داخل الصنف، توجد دالة ()getAnimalInfo كل عملها هو إعادة معلومات نوع حيوان واحد. لذلك، لدى صنف Animal خاصيّة واحدة، وهي من نوع كائن الحيوان الذي سيتم طباعة معلوماته، عند إرساله كمعامل. ثم لدينا ثلاث أصناف بيانات تُمثل ثلاث حيوانات مختلفة.

ما نريده، هو إنشاء كائن “حيوان” مختلف في كل مرة باستخدام الصنف Animal، بإرسال الصنف الذي يُمثل هذا الحيوان، وخاصيّة الصوت الخاص به. ثم استدعاء دالة ()getAnimalInfo، لطباعة معلومات الحيوان.

في المرة الأولى، أنشأنا كائن “قط” باستخدام الصنف Cat وخاصيّة الصوت الخاص به، وأرسلناه إلى الصنف Animal كمعامل. هنا لن تحدث مشكلة لأننا وضعنا نوع بيانات معامل الصنف Animal من النوع Cat. ولكن، عند محاولة إنشاء كائن حيوان من نوع آخر، الأسد Lion مثلًا، سيظهر الخطأ التالي، في برنامج IntelliJ:

Type mismatch. Required: Cat Found: Lion

يُوضِّح الخطأ، عدم التطابق في القيمة Argument المُرسلة لمعامل Animal، ونوع بيانات المعامل. فالمطلوب كائن من النوع Cat ولكننا أرسلنا كائن من النوع Lion. بالطبع من الممكن وضع الصنف Lion كخاصيّة للصنف Animal، ولكن حينها لن نستطيع إرسال كائن Cat.

المعامل من النوع T:

لحل المشكلة، وتمكين الصنف Animal من أن يستقبل أي نوع بيانات أو صنف خاص، علينا أن نجعل معامله من النوع عام، بإستخدام الحرف T:

class Animal<T>(private val obj: T) {

    fun getAnimalInfo() = println(obj)

}

يجب وضع الحرف T بين قوسي زاوية < > بعد اسم الصنف Animal أولاً، ثم تغيير نوع بيانات معامل الصنف، إلى النوع T. بهذه الطريقة، نكون قد جعلنا من الصنف Animal، صنف عام يمكنه إستقبال أي كائن من أي نوع بيانات في كوتلن.

والآن، يمكننا إنشاء كائن بهذه الطريقة:

val cat: Animal<Cat> = Animal(Cat(“Meow, Meow”))

نخبر كوتلن بنوع الكائن الذي سيُمثله الصنف Animal في هذه اللحظة، بوضع اسم النوع صراحةً: 

Animal<Cat>

هذه الطريقة صحيحة. ولكن لأن كوتلن ستستنتج النوع من الكائن المُرسَل إلى الصنف Animal، يمكننا التخلي عن كتابة اسم النوع صراحةً، لأنه يعتبر تكرار لا داعي له.

وعند تعديل الشفرة وتنفيذها، سيتم طباعة معلومات أي كائن يتم إرساله إلى معامل الصنف، باستخدام الدالة ()getAnimalInfo المتواجدة في الصنف Animal:

الدوال المُعمَمّة Generic Functions:

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

وتكون أبسط صورة للإعلان عن هذه الدوال:

fun <T> doSomething(t: T): T {

    return t

}

الكلمة المفتاحية للدوال fun، بعدها الحرف T بين قوسي زاوية < >، ثم اسم الدالة، ثم المعامل من النوع T. ويمكننا أيضًا، جعل نوع إرجاع ReturnType الدالة، من النوع عام.

دالة للبحث عن عنصر في قائمة:

كمثال، إذا كنا نريد دالة تبحث لنا عن عنصر من أي نوع في قائمة من أي نوع، يجب أن نجعلها دالة عامة:

لدى الدالة معاملين أحدهما element وهو من النوع عام T. يمكننا إرسال كائن من أي نوع بيانات كقيمة Argument لهذا المعامل. أما المعامل الثاني list، فهو ليس نوع عام داخل الدالة.

بمعنى يجب أن نرسل حصريا قيمة من التجميعة List لهذا المعامل. ولن يقبل هذا المعامل، حتى قائمة قابلة للتعديل MutableList. أو غيرها من التجميعات أو أنواع البيانات.

ولكن، العناصر التي سنضعها في هذه القائمة، هي التي يمكن أن تكون من أي نوع. أي أن المعامل list يمكن أن تكون عناصره الداخلية من أي نوع. لأن الـ T هنا تخص الصنف List من مكتبة كوتلن القياسية.

وأيضاً عند عدم وجود العنصر في القائمة المُرسلة للدالة، فستكون نتيجة البحث null. لتجنب المشاكل الناتجة عن إحتمال عدم وجود قيمة، استفدنا من ميزة الـ nullable في كوتلن، وجعلنا نوع الإرجاع في الدالة ?T. لتمكين الدالة من إرجاع النتيجة null، ثم إلتقاطها والتعامل معها عبر استخدام عامل Evis (:?)، والذي درسناه في الدرسين السابقين.

والآن يمكننا استدعاء هذه الدالة، وإرسال العنصر المُراد البحث عنه، وقائمة تحتوي على عناصر من أي نوع بيانات، كقيم لمعامليها:

أرسلنا للدالة، قائمة من النوع Int، وقائمة من النوع Char، وقائمة من النوع String. وفي كل مرة ستتعامل الدالة مع القائمة المُرسلة إليها، وتبحث عن العنصر المُراد البحث عنه، وتعيد العنصر إذا كان موجودا في القائمة، أو null.

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

الدوال المُلحقة المُعمَمّة Generic Extension Functions:

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

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

تعتبر هذه الدالة جزء من الصنف List في مشروع برنامجنا الحالي. والصنف List هو بدوره يمكنه أن يستقبل نوع بيانات عام. الكلمة this، تشير إلى كائن التجميعة List الذي يستدعي الدالة ()find. لذلك يمكننا استخدام كتلة for، للدوران على عناصر القائمة التي تمثلها الكلمة this، ومقارنتها مع العنصر المرسل الدالة كمعامل.

نستطيع الآن استدعاء الدالة باستخدام اسم الصنف ثم النقطة (.)، تمامًا كما نفعل مع الدوال المنتمية للصنف Member Functions، لأن مترجم كوتلن يعتبرها، جزء من الصنف المُلحقة به:

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

تسمية الأنواع المُعمَمّة:

حتى الآن في هذا الدرس، استخدمنا الحرف T للإعلان عن النوع العام Generic. لأنه أول حرف من الكلمة Type، لذا من المنطقي استخدامه. ولكن، ليس لزامًا أن نستخدم هذا الحرف تحديدًا، بل يمكننا استخدام أي حرف أو أية كلمة. 

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

  • T: الحرف الأول من الكلمة Type.
  • S, U, V: إذا كان الصنف يحتوي على أكثر من نوع عام، نبدأ بالحرف T للنوع الأول ثم هذه الحروف بالترتيب لكل نوع لاحق. سنشرح هذا الأمر، في الفقرة التالية.
  • E: الحرف الأول من الكلمة Element. يستخدم غالبًا مع التجميعات.
  • K: الحرف الأول من Key.
  • V: الحرف الأول من كلمة Value.
  • N: الحرف الأول من Number.

استخدام أكثر من نوع عام واحد:

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

تسمح لنا كوتلن بفعل ذلك، باستخدام عدة أحرف، يُمثل كل منها نوع عام واحد. ويجب الفصل بين هذه الأنواع، بفاصلة ( , ):

class Three<T, U, V>(private val first: T, private val second: U, private val third: V)

أنشأنا صنف خاص أسميناه Three، للدلالة أنه سيحتوي على عناصر من ثلاث أنواع بيانات. الآن يمكننا إنشاء كائن من الصنف Three، باستخدام ثلاث أنواع بيانات مختلفة، أو حتى متشابهة.

class Pair<A, B>(private val first: A, private val second: B)

أمّا الصنف Pair، هو الآخر صنف خاص بنا والذي يمكنه أن يحتوي على عنصري بيانات. فيُمكننا أن ننشئ منه كائن باستخدام نوعي بيانات مختلفين أو متشابهين.

عند استدعاء هذين الصنفين، يمكننا إرسال العدد المناسب للمعاملات الخاصة بهما:

استخدمنا دالة ()toString، لطباعة معلومات الصنف بطريقة مخصصة. وتم شرح كيفية فعل ذلك تفصيليًا، في درس أصناف البيانات Data Classes

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

ملاحظة:

يتوفر في مكتبة كوتلن القياسية، صنفين يحملان الإسم Triple لتمثيل ثلاث أنواع بيانات (ثلاث كائنات) و Pair لتمثيل نوعي بيانات (كائنين).

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

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