Generics في Kotlin: دليل شامل لكتابة كود آمن ومرن

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: لماذا تعتبر Generics أداة سهلة ومفيدة وليست مخيفة

\n\n

تخيل أنك تكتب كلاس باسم «صندوق». يمكنك وضع أي شيء فيه: كتاب، تفاحة، هاتف. لكن إذا جعلته من النوع Object، ففي كل مرة تخرج فيها شيئًا ستضطر لاستخدام as? Book وتأمل ألا يحدث خطأ ClassCastException. هل هذا مألوف؟

\n\n

هنا تأتي Generics (الأنواع العامة) لإنقاذك — وهي آلية قوية للتحكم في الأنواع في Kotlin، تتيح لك كتابة كود مرن دون التضحية بأمان الأنواع. في هذا المقال، سنشرح Generics في Kotlin من البسيط إلى المعقد: الكلاسات الأساسية، الدوال، Variance (in/out)، الأنواع المعاد تعريفها (Reified Types)، والإسقاطات النجمية (Star Projections). ستتعلم كيف تجعل كودك أنظف، والمُجمّع (Compiler) حليفك الأكبر.

\n\n

هذا المقال مُعد لبوابة pythonlib.ru التعليمية — دليلك إلى عالم لغات البرمجة الحديثة.

\n\n

1. الأساسيات: ما هي Generics في Kotlin؟

\n\n

Generics (الأنواع العامة) تسمح بجعل الكلاسات، الواجهات، والدوال مُعلمة بنوع يُحدد عند الاستخدام. هذا يمنع تكرار الكود ويمنحك تحكمًا في الأنواع أثناء مرحلة التجميع (Compile Time).

\n\n

ألقِ نظرة على هذا المثال البسيط:

\n\n
class Box<T>(val item: T)\n\nfun main() {\n    val bookBox = Box<String>("Kotlin in Action")\n    val intBox = Box<Int>(42)\n    // المُجمّع يعرف النوع!\n    val title: String = bookBox.item\n}
\n\n

نقاط رئيسية:

\n
    \n
  • <T> — معامل النوع (يمكن أن يكون أي حرف، لكن الاصطلاح هو T, R, E, K, V).
  • \n
  • يتم تثبيت النوع عند إنشاء الكائن — لا حاجة للتحويل (Casting) في وقت التشغيل.
  • \n
  • تعمل Generics فقط في مرحلة التجميع (Type Erasure)، لكن Kotlin يضيف «إعادة التعريف» (Reification) (سنتحدث عنها لاحقًا).
  • \n
\n\n

الدوال العامة (Generic Functions)

\n

التحكم في الأنواع في Kotlin يعمل أيضًا مع الدوال. على سبيل المثال، دالة تُرجع أول عنصر من قائمة:

\n\n
fun <T> firstItem(list: List<T>): T? = list.firstOrNull()\n\nfun main() {\n    val numbers = listOf(1, 2, 3)\n    val first: Int? = firstItem(numbers) // T = Int\n    println(first) // 1\n}
\n\n

لاحظ: Kotlin يستطيع استنتاج النوع من السياق — ليس من الضروري كتابة firstItem<Int>(numbers).

\n\n

2. Variance: in و out — كيف لا تكسر أمان الأنواع

\n\n

أصعب موضوع للمبتدئين هو Variance (التباين). يجيب على السؤال: هل يمكن أن يكون List<String> نوعًا فرعيًا من List<Any>؟

\n\n

في Java، يُحل هذا باستخدام Wildcards (? extends / ? super). في Kotlin، باستخدام out و in.

\n\n

out (التباين المشترك) — للقراءة فقط

\n

العلامة out تعني أن النوع العام يمكن فقط «إخراجه» (إرجاعه من الدوال). مثال: List<out T>.

\n\n
interface Source<out T> {\n    fun next(): T  // صحيح: T في المخرج\n    // fun add(item: T) // خطأ! T في المدخل ممنوع\n}\n\nfun demo(source: Source<String>) {\n    val anySource: Source<Any> = source // out يسمح بهذا\n}
\n\n

لماذا هذا آمن؟ إذا كان بإمكاننا فقط قراءة String من المصدر، فيمكن اعتباره مصدرًا لـ Any.

\n\n

in (التباين المعاكس) — للكتابة فقط

\n

in — العكس: النوع يمكن أن يكون فقط في المدخل. المثال الكلاسيكي: Comparable.

\n\n
interface Comparable<in T> {\n    operator fun compareTo(other: T): Int\n}\n\nfun demo(comp: Comparable<Number>) {\n    val intComp: Comparable<Int> = comp // in يسمح بهذا\n}
\n\n

إذا كان Comparable<Number> يستطيع مقارنة أي أرقام، فبالتأكيد سيستطيع مقارنة Int أيضًا.

\n\n

in out (التباين الثابت) — الوضع الافتراضي

\n

إذا لم يتم تحديد in أو out، فإن النوع يكون ثابتًا (Invariant). هذا يعني أن Box<String> ليس نوعًا فرعيًا من Box<Any>، والعكس صحيح. هذا هو السلوك الافتراضي والأكثر أمانًا.

المدونات

توصيات الكتب