الأنواع المُعمَّمة (Generics) في لغة Kotlin

من موسوعة حسوب
مراجعة 17:08، 13 مارس 2018 بواسطة Nourtam (نقاش | مساهمات) (أنشأ الصفحة ب'<noinclude>{{DISPLAYTITLE:الأنواع المُعمَّمة (Generics) في لغة Kotlin}}</noinclude> قد تحتوي الأصناف (classes) -كما هو الحا...')
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)

قد تحتوي الأصناف (classes) -كما هو الحال في لغة Java- على متحولاتٍ للأنواع (type parameters) مثل:

class Box<T>(t: T) {
    var value = t
}

ولإنشاء كائنٍ (instance) من هذا الصنف يجب تحديد النوع كما في الشيفرة الآتية:

val box: Box<Int> = Box<Int>(1)

أمّا إن كان بالإمكان معرفة (infer) المتحولات إمّا من خلال متحولات الباني (constructor arguments) أو بأيّ وسيلةٍ أخرى فيمكن حينئذٍ حذف نوع المتحولات، مثل:

val box = Box(1) //  القيمة 1 لها نوع الأعداد الصحيحة 
                 // وبالتالي فإن المترجم سيحدد تلقائيًا النوع 
                 // Box<Int>

التغيُّر (Variance)

إن نظام الأنواع (types system) واحدٌ من أكثر الأمور صعوبةً في لغة Java إذ تعتمد على الأنواع المُوسَّعة (wildcard)، ولا تدعم لغة Kotlin هذه الأنواع بل تعتمد بدلًا من ذلك: التغيُّر في موقع التصريح (declaration-site variance) وتوقُّع الأنواع (type projection).

ولكن لو تساءلنا: ما الذي دفع Java إلى الاعتماد على هذه الأنواع الموسَّعة؟ والجواب يكمُن بزيادة مرونة واجهات API ؛ إذ إنّ الأنواع المُعمَّمة في لغة Java ثابتة (invariant) وبالتالي لا يُعدُّ النوع List<String>‎ نوعًا فرعيًا (subtype) من النوع List<Object>‎ فإذا كانت القوائم غير ثابتة (not invariant) فلن تتميّز بشيء عن المصفوفات، وسينتج استثناء (exception) عند محاولة تنفيذ الشيفرة الآتية:

// Java شيفرة
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // السبب الرئيسي بالخطأ البرمجي هو هذه التعليمة ولا يُسمح بمثل هذا الإسناد

objs.add(1); // هنا توضع القيمة الصحيحة 1 في قائمة من السلاسل النصية

String s = strs.get(0); // استثناء من نوعClassCastException
                        // لا يمكن تحويل قيمة العدد الصحيح إلى سلسلة نصيّة

وبالتالي فإن Java لا تسمح بمثل هذه التعليمات لضمان الأمان أثناء التنفيذ (run-time safety)، وقد يسبِّب ذلك بعضَ النتائج، فبفرض أنّ للتابع addAll()‎ من الواجهة Collection مثلًا الترويسة:

// Java
interface Collection<E> ... {
  void addAll(Collection<E> items);
}

فلن نستطيع القيام بما في الشيفرة الآتية على الرغم من أنها آمنة تمامًا:

// Java شيفرة
void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from); // لن يُترجم بالتصريح السابق للدالة addAll
                   // لأن النوع Collection<String>
                   // ليس نوعًا فرعيًا من Collection<Object>

}

لذلك تُفضَّل القوائم (lists) في Java على المصفوفات (arrays) وبالتالي فإن ترويسة التابع addAll()‎ في Java تكون بالشكل:

// Java شيفرة
interface Collection<E> ... {
  void addAll(Collection<? extends E> items);
}

إن متحول النوع المُوسَّع ‎? extends E يدلُّ بأنّ هذا التابع (method) يقبل مجموعةً من الكائنات من E أو أي نوعٍ فرعيّ (subtype) منه وليس فقط النوع E بحدّ ذاته، وبالتالي تُمكن قراءة قيم النوع E من العناصر (عناصر هذه المجموعة هي كائنات من الصنف الفرعي من E)، ولكن من غير الممكن الكتابة فيها لأنّه لا يمكن تحديد أيٍّ من الكائنات تتناسب مع ذلك النوع الفرعي غير المعروف من E، وبالمقابل؛ يُعدُّ النوع Collection<String>‎ نوعًا فرعيًّا من Collection<? extends Object>‎ وبالتالي فإنّ التوسعة باستخدام extend (توسعة علوية [upper]) تجعل النوع متغايرًا (covariant).

والسبيل لفهم آلية العمل بمثل هذه الحالات بسيط؛ إن كان بإلإمكان الحصول فقط على العناصر من مجموعة (collection) فإنه من السهل استخدام مجموعةٍ من السلاسل النصية (String) وقراءة كائنات منها، وبالمقابل إن كان بإلإمكان -فقط- وضع العناصر في المجموعة فإنه من الممكن إنشاء مجموعة من الكائنات (Object) ووضع السلاسل النصية (String) فيها، لأن النوع List<? super String>‎ هو نوع أعلى للنوع List<Object>‎ والذي يُدعى (contravariance)، إذ يمكن استدعاء التوابع التي تقبل النوع String كمتحول في List<? super String>‎ فقط (أي يمكن استدعاء add(String)‎ أو set(int, String)‎)، أما عند استدعاء أيّ تابعٍ يعيد النوع T في List<T>‎ فلن تحصل على النوع String بل النوع Object.

وتُسمَّى الكائنات المُتاحة للقراءة فقط بالمُنتِجات (Producers) وتلك المتاحة للكتابة بالمستهلكات (Consumers)، وبتعبيرٍ أفضل: Producer-Extends, Consumer-Super.

لاحظ أنّه عند استخدام الكائن المنتِج وليكن List<? extends Foo>‎ فلا يمكن استدعاء التابعadd()‎ أو set()‎ عبر هذا الكائن، ولكن ذلك لا يعني أنّ هذا الكائن ثابتٌ فلا يوجد ما يمنع استدعاء clear()‎ لإزالة كلِّ العناصر من القائمة، وذلك لأن الدالة clear()‎ لا تتطلَّب وجود أيّة متحولاتٍ (parameters)، والأمر الوحيد الذي تضمنه الأنواع المُوسَّعة (wildcards) هو الأمان في النوع (type safety) أما الثبات (immutability) فهو أمرٌ آخر مختلف.