الفرق بين المراجعتين لصفحة: «Kotlin/generics»
إضافة فقرة جديدة |
إضافة فقرات |
||
سطر 1: | سطر 1: | ||
<noinclude>{{DISPLAYTITLE:الأنواع المُعمَّمة (Generics) في لغة Kotlin}}</noinclude> | <noinclude>{{DISPLAYTITLE:الأنواع المُعمَّمة (Generics) في لغة Kotlin}}</noinclude> | ||
== متحوِّلات الأنواع (Type Parameters) == | |||
قد تحتوي الأصناف (classes) -كما هو الحال في لغة Java- على متحولاتٍ للأنواع (type parameters) مثل:<syntaxhighlight lang="kotlin"> | قد تحتوي الأصناف (classes) -كما هو الحال في لغة Java- على متحولاتٍ للأنواع (type parameters) مثل:<syntaxhighlight lang="kotlin"> | ||
class Box<T>(t: T) { | class Box<T>(t: T) { | ||
سطر 11: | سطر 13: | ||
// Box<Int> | // Box<Int> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==التغيُّر (Variance)== | |||
== التغيُّر (Variance) == | إن نظام الأنواع (types system) واحدٌ من أكثر الأمور صعوبةً في لغة Java إذ تعتمد على الأنواع المُوسَّعة (wildcard)، ولا تدعم لغة Kotlin هذه الأنواع بل تعتمد بدلًا من ذلك: التغيُّر في موقع التصريح (declaration-site variance) والأنواع المُسقَطة (type projection). | ||
إن نظام الأنواع (types system) واحدٌ من أكثر الأمور صعوبةً في لغة Java إذ تعتمد على الأنواع المُوسَّعة (wildcard)، ولا تدعم لغة Kotlin هذه الأنواع بل تعتمد بدلًا من ذلك: التغيُّر في موقع التصريح (declaration-site variance) | |||
ولكن لو تساءلنا: ما الذي دفع Java إلى الاعتماد على هذه الأنواع الموسَّعة؟ والجواب يكمُن بزيادة مرونة واجهات API ؛ إذ إنّ الأنواع المُعمَّمة في لغة Java ثابتة (invariant) وبالتالي لا يُعدُّ النوع <code>List<String></code> نوعًا فرعيًا (subtype) من النوع <code>List<Object></code> فإذا كانت القوائم غير ثابتة (not invariant) فلن تتميّز بشيء عن المصفوفات، وسينتج استثناء (exception) عند محاولة تنفيذ الشيفرة الآتية:<syntaxhighlight lang="java"> | ولكن لو تساءلنا: ما الذي دفع Java إلى الاعتماد على هذه الأنواع الموسَّعة؟ والجواب يكمُن بزيادة مرونة واجهات API ؛ إذ إنّ الأنواع المُعمَّمة في لغة Java ثابتة (invariant) وبالتالي لا يُعدُّ النوع <code>List<String></code> نوعًا فرعيًا (subtype) من النوع <code>List<Object></code> فإذا كانت القوائم غير ثابتة (not invariant) فلن تتميّز بشيء عن المصفوفات، وسينتج استثناء (exception) عند محاولة تنفيذ الشيفرة الآتية:<syntaxhighlight lang="java"> | ||
سطر 43: | سطر 44: | ||
void addAll(Collection<? extends E> items); | void addAll(Collection<? extends E> items); | ||
} | } | ||
</syntaxhighlight>إن متحول النوع المُوسَّع <code>? extends E</code> يدلُّ بأنّ هذا التابع (method) يقبل مجموعةً من الكائنات من <code>E</code> أو أي نوعٍ فرعيّ (subtype) منه وليس فقط النوع <code>E</code> بحدّ ذاته، وبالتالي تُمكن قراءة قيم النوع <code>E</code> من العناصر (عناصر هذه المجموعة هي كائنات من الصنف الفرعي من <code>E</code>)، ولكن من غير الممكن الكتابة فيها لأنّه لا يمكن تحديد أيٍّ من الكائنات تتناسب مع ذلك النوع الفرعي غير المعروف من <code>E</code>، وبالمقابل؛ يُعدُّ النوع <code>Collection<String></code> نوعًا فرعيًّا من <code>Collection<? extends Object></code> وبالتالي فإنّ التوسعة باستخدام <code>extend</code> ( | </syntaxhighlight>إن متحول النوع المُوسَّع <code>? extends E</code> يدلُّ بأنّ هذا التابع (method) يقبل مجموعةً من الكائنات من <code>E</code> أو أي نوعٍ فرعيّ (subtype) منه وليس فقط النوع <code>E</code> بحدّ ذاته، وبالتالي تُمكن قراءة قيم النوع <code>E</code> من العناصر (عناصر هذه المجموعة هي كائنات من الصنف الفرعي من <code>E</code>)، ولكن من غير الممكن الكتابة فيها لأنّه لا يمكن تحديد أيٍّ من الكائنات تتناسب مع ذلك النوع الفرعي غير المعروف من <code>E</code>، وبالمقابل؛ يُعدُّ النوع <code>Collection<String></code> نوعًا فرعيًّا من <code>Collection<? extends Object></code> وبالتالي فإنّ التوسعة باستخدام <code>extend</code> (بالحدِّ العلوي [upper]) تجعل النوع covariant. | ||
والسبيل لفهم آلية العمل بمثل هذه الحالات بسيط؛ إن كان بإلإمكان الحصول فقط على العناصر من مجموعة (collection) فإنه من السهل استخدام مجموعةٍ من السلاسل النصية (<code>String</code>) وقراءة كائنات منها، وبالمقابل إن كان بإلإمكان -فقط- وضع العناصر في المجموعة فإنه من الممكن إنشاء مجموعة من الكائنات (<code>Object</code>) ووضع السلاسل النصية (<code>String</code>) فيها، لأن النوع <code>List<? super String></code> هو نوع أعلى للنوع <code>List<Object></code> والذي يُدعى (contravariance)، إذ يمكن استدعاء التوابع التي تقبل النوع <code>String</code> كمتحول في <code>List<? super String></code> فقط (أي يمكن استدعاء <code>add(String)</code> أو <code>set(int, String)</code>)، أما عند استدعاء أيّ تابعٍ يعيد النوع <code>T</code> في <code>List<T></code> فلن تحصل على النوع <code>String</code> بل النوع <code>Object</code>. | والسبيل لفهم آلية العمل بمثل هذه الحالات بسيط؛ إن كان بإلإمكان الحصول فقط على العناصر من مجموعة (collection) فإنه من السهل استخدام مجموعةٍ من السلاسل النصية (<code>String</code>) وقراءة كائنات منها، وبالمقابل إن كان بإلإمكان -فقط- وضع العناصر في المجموعة فإنه من الممكن إنشاء مجموعة من الكائنات (<code>Object</code>) ووضع السلاسل النصية (<code>String</code>) فيها، لأن النوع <code>List<? super String></code> هو نوع أعلى للنوع <code>List<Object></code> والذي يُدعى (contravariance)، إذ يمكن استدعاء التوابع التي تقبل النوع <code>String</code> كمتحول في <code>List<? super String></code> فقط (أي يمكن استدعاء <code>add(String)</code> أو <code>set(int, String)</code>)، أما عند استدعاء أيّ تابعٍ يعيد النوع <code>T</code> في <code>List<T></code> فلن تحصل على النوع <code>String</code> بل النوع <code>Object</code>. | ||
سطر 50: | سطر 51: | ||
<span> </span>لاحظ أنّه عند استخدام الكائن المنتِج وليكن <code>List<? extends Foo></code> فلا يمكن استدعاء التابع<code>add()</code> أو <code>set()</code> عبر هذا الكائن، ولكن ذلك لا يعني أنّ هذا الكائن ثابتٌ فلا يوجد ما يمنع استدعاء <code>clear()</code> لإزالة كلِّ العناصر من القائمة، وذلك لأن الدالة <code>clear()</code> لا تتطلَّب وجود أيّة متحولاتٍ (parameters)، والأمر الوحيد الذي تضمنه الأنواع المُوسَّعة (wildcards) هو الأمان في النوع (type safety) أما الثبات (immutability) فهو أمرٌ آخر مختلف. | <span> </span>لاحظ أنّه عند استخدام الكائن المنتِج وليكن <code>List<? extends Foo></code> فلا يمكن استدعاء التابع<code>add()</code> أو <code>set()</code> عبر هذا الكائن، ولكن ذلك لا يعني أنّ هذا الكائن ثابتٌ فلا يوجد ما يمنع استدعاء <code>clear()</code> لإزالة كلِّ العناصر من القائمة، وذلك لأن الدالة <code>clear()</code> لا تتطلَّب وجود أيّة متحولاتٍ (parameters)، والأمر الوحيد الذي تضمنه الأنواع المُوسَّعة (wildcards) هو الأمان في النوع (type safety) أما الثبات (immutability) فهو أمرٌ آخر مختلف. | ||
==التغيُّر في موقع التصريح (Declaration-site Variance)== | |||
== التغيُّر في موقع التصريح (Declaration-site Variance) == | |||
بفرض وجود الواجهة المُعمَّمة (generic interface) <code>Source<T></code> غير المحتوية على أيّ تابعٍ (method) له متحولاتٌ من النوع <code>T</code> وإنما تحتوي فقط على توابعَ تعيد النوع <code>T</code>، بالشكل:<syntaxhighlight lang="java"> | بفرض وجود الواجهة المُعمَّمة (generic interface) <code>Source<T></code> غير المحتوية على أيّ تابعٍ (method) له متحولاتٌ من النوع <code>T</code> وإنما تحتوي فقط على توابعَ تعيد النوع <code>T</code>، بالشكل:<syntaxhighlight lang="java"> | ||
// Java شيفرة | // Java شيفرة | ||
سطر 74: | سطر 74: | ||
// ... | // ... | ||
} | } | ||
</syntaxhighlight>'''القاعدة العامّة:''' إن عُرِّفَ متحول النوع <code>T</code> من الصنف <code>C</code> بالمُحدِّد <code>out</code> فيُسمَح له أن يكون نوعًا "صادرًا" فقط عن عناصر <code>C</code>، وبالمقابل من الآمن أن يكون <code>C<Base></code> نوعًا أعلى (supertype) من <code>C<Derived></code>، وبصياغةٍ أخرى: يُعدُّ الصنف <code>C</code> | </syntaxhighlight>'''القاعدة العامّة:''' إن عُرِّفَ متحول النوع <code>T</code> من الصنف <code>C</code> بالمُحدِّد <code>out</code> فيُسمَح له أن يكون نوعًا "صادرًا" فقط عن عناصر <code>C</code>، وبالمقابل من الآمن أن يكون <code>C<Base></code> نوعًا أعلى (supertype) من <code>C<Derived></code>، وبصياغةٍ أخرى: يُعدُّ الصنف <code>C</code> من النوع covariant في المتحول <code>T</code> أو إن <code>T</code> متحولُ نوعٍ covariant، و'''خلاصة القول:''' يكون <code>C</code> منتِجًا للنوع <code>T</code> لا مستهلِكًا له. | ||
ويأتي دعم لغة Kotlin للتغيُّر في موقع التصريح (declaration-site variance) عبر المُحدِّد <code>out</code> على عكس ما في لغة Java ، والذي هو التغيُّر في استخدام التصريح (use-site variance) حيث تجعل توسعةُ الأنواع (wildcards) الأنواعَ | ويأتي دعم لغة Kotlin للتغيُّر في موقع التصريح (declaration-site variance) عبر المُحدِّد <code>out</code> على عكس ما في لغة Java ، والذي هو التغيُّر في استخدام التصريح (use-site variance) حيث تجعل توسعةُ الأنواع (wildcards) الأنواعَ covariant. | ||
كما أنّ Kotlin تدعم المُحدِّد <code>in</code> مما يجعل متحول النوع contravariant، أي أنّه من المُمكن أن يكون مستهلَكًا لا منتَجًا، ويُعدُّ النوع <code>Comparable</code> خير مثالٍ عنه كما في الشيفرة الآتية:<syntaxhighlight lang="kotlin"> | كما أنّ Kotlin تدعم المُحدِّد <code>in</code> مما يجعل متحول النوع contravariant، أي أنّه من المُمكن أن يكون مستهلَكًا لا منتَجًا، ويُعدُّ النوع <code>Comparable</code> خير مثالٍ عنه كما في الشيفرة الآتية:<syntaxhighlight lang="kotlin"> | ||
سطر 92: | سطر 92: | ||
} | } | ||
</syntaxhighlight>وقد استُخدِم المُحدِّدان <code>in</code> و <code>out</code> في لغة C# لبعض الوقت. | </syntaxhighlight>وقد استُخدِم المُحدِّدان <code>in</code> و <code>out</code> في لغة C# لبعض الوقت. | ||
==الأنواع المُسقَطة (Type Projection)== | |||
=== التغيُّر في موقع الاستخدام (Use-site Variance): الأنواع المُسقَطة (Type Projection) === | |||
من السهل التصريحُ عن متحوِّل النوع <code>T</code> كنوع <code>out</code> وتجنُّب المشاكل الناتجة عن الأصناف الفرعيّة في مكان استخدامها، ولكن تقتصر بعض الأصناف على النوع <code>T</code> كنوعٍ مُعادٍ، كما هو الحال في المصفوفات (Array) بالشيفرة الآتية:<syntaxhighlight lang="kotlin"> | |||
class Array<T>(val size: Int) { | |||
fun get(index: Int): T { /* ... */ } | |||
fun set(index: Int, value: T) { /* ... */ } | |||
} | |||
</syntaxhighlight>فلا يُمكن أن يكون هذا الصنف من النوع covariant ولا contravariant (المشروحين سابقًا في هذه الصفحة) بالنسبة لمتحول النوع <code>T</code>، وهذا بالمقابل لا يمنح مرونةً في البرمجة، فإن كانت الدالة الآتية:<syntaxhighlight lang="kotlin"> | |||
fun copy(from: Array<Any>, to: Array<Any>) { | |||
assert(from.size == to.size) | |||
for (i in from.indices) | |||
to[i] = from[i] | |||
} | |||
</syntaxhighlight>يتَّضِح من الشيفرة السابقة أنّ هذه الدالة تنسخ العناصر من مصفوفةٍ لأخرى، ولدى تجربتها واقعيًا:<syntaxhighlight lang="kotlin"> | |||
val ints: Array<Int> = arrayOf(1, 2, 3) | |||
val any = Array<Any>(3) { "" } | |||
copy(ints, any) // سينتج خطأ لأن من المتوقع أن تكون الأنواع | |||
// (Array<Any>, Array<Any>) | |||
</syntaxhighlight>ونعود للمشكلة الشائعة هنا؛ وهي أنّ <code>Array<T></code> ثابتة (invariant) في <code>T</code> وبالتالي فليس أيٌّ من النوعين <code>Array<Int></code> و <code>Array<Any></code> صنفًا فرعيًا (subtype) للآخر، ولكن لِمَ؟ لأنّ عملية النسخ قد تجلب بعض المتاعب البرمجية مثل محاولة كتابة سلسلة نصيّة (string) في المصفوفة <code>from</code> ، وبالتالي إذا مُرِّرت مصفوفة أعدادٍ صحيحةٍ <code>Int</code> هناك فقد ينتُج استثناء <code>ClassCastException</code> فيما بعد. | |||
ولذلك يجب ضمان أمرٍ واحدٍ وهو أنّ دالة <code>copy()</code> لا تحتمِل حدوث أيّ مشكلة ويجب إذن منعها من الكتابة في <code>from</code> كما في الشيفرة الآتية:<syntaxhighlight lang="kotlin"> | |||
fun copy(from: Array<out Any>, to: Array<Any>) { | |||
// ... | |||
} | |||
</syntaxhighlight>وما يحدث هنا يُسمّى بالأنواع المُسقَطة (type projection) بحيث يمكن القول بأن <code>from</code> مصفوفةٌ لكنها مُسقَطةٌ محدودةٌ إذ يمكن استدعاء تلك التوابع (methods) التي تعيد متحوِّل النوع <code>T</code> فقط، مما يعني أنّه بالإمكان الاستفادة من <code>()get</code> فقط، وهذه طريقة Kotlin في تغيُّر موقع الاستخدام (use-site variance) وهي تُماثل <code>Array<? extends Object></code> في Java ولكن بطريقة أبسط. | |||
كما ويُتاح إسقاطُ نوعٍ ما باستخدام المعامل <code>in</code> كما في الشيفرة الآتية:<syntaxhighlight lang="kotlin"> | |||
fun fill(dest: Array<in String>, value: String) { | |||
// ... | |||
} | |||
</syntaxhighlight>إذ توافق الشيفرةُ <code>Array<in String></code> الشيفرةَ <code>Array<? super String></code> في Java، أي يمكن تمريرُ مصفوفةٍ من <code>CharSequence</code> أو مصفوفةٍ من <code>Object</code> للدالة <code>fill()</code>. | |||
== | === الإسقاط الواسع (Star-projection) === | ||
قد لا تتوافر -في بعض الأحيان- معلوماتٌ كافيةٌ عن متحوِّل النوع (type argument) ولا بُدَّ من استخدامه بشكل آمنٍ، يتمُّ ذلك عن طريق تعريف إسقاطٍ من النوع المُعمَّم (generic) بحيث يكون كل كائنٍ (instance) فعليٍّ من هذا النوع المُعمَّم نوعًا فرعيًا (subtype) من هذا الإسقاط. | |||
وتوفِّر Kotlin صيغة الإسقاط الواسع لكلِّ من: | |||
* <code>Foo<out T : TUpper></code> إذ يكون <code>T</code> نوعًا covariant عبر الحدِّ العلوي <code>TUpper</code> وتكون الصيغة <code>Foo<*></code> مكافئةً للصيغة <code>Foo<out TUpper></code> مما يعني أنّه عندما يكون النوع <code>T</code> غير معروفٍ فمن الآمن قراءةُ قيم <code>TUpper</code> من <code>Foo<*></code>. | |||
* <code>Foo<in T></code> إذ يكون <code>T</code> من النوع contravariant وتكون الصيغة <code>Foo<*></code> مكافئةً للصيغة <code>Foo<in Nothing></code> ممّا يعني أنه عندما يكون النوع <code>T</code> غير معروفٍ فلا وجود لما يُكتب بطريقة آمنة في <code>Foo<*></code>. | |||
* <code>Foo<T : TUpper></code> إذ يكون <code>T</code> نوعًا ثابتًا (invariant) عبر الحدِّ العلوي<code>TUpper</code> وتكون الصيغة <code>Foo<*></code> مكافئةً للصيغة <code>Foo<out TUpper</code>> لقراءة القيم ومكافئةً للصيغة <code>Foo<in Nothing></code> لكتابتها. | |||
ولدى وجود عدّة متحوِّلات للأنواع لنفس النوع المُعمَّم (generic) فيمكن حينئذٍ أن تُسقَط بشكلٍ مستقِّلٍ، فإن كان النوع مثلًا مُصرَّحًا بالشكل: <code>interface Function<in T, out U></code> تكون الإسقاطات الواسعة حينها بالشكل: | |||
* <code>Function<*, String></code> والتي تعني <code>Function<in Nothing, String></code> | |||
* <code>Function<Int, *></code> والتي تعني <code>Function<Int, out Any?></code> | |||
* <code>Function<*, *></code> والتي تعني <code>Function<in Nothing, out Any?></code> | |||
وهي تُماثِل بذلك الأنواع الخامّ في Java ولكنها آمنة (safe). | |||
== | ==الدوال المُعمَّمة (Generic Functions)== | ||
قد يكون للدوال أيضًا متحولاتٌ للأنواع تُكتب قبل اسم الدالة بالشكل:<syntaxhighlight lang="kotlin"> | |||
fun <T> singletonList(item: T): List<T> { | |||
// ... | |||
} | |||
== | fun <T> T.basicToString() : String { // دالة إضافية extension | ||
// ... | |||
} | |||
</syntaxhighlight>ولاستدعاء الدالة المُعمَّمة يجب تحديد متحولات النوع في الاستدعاء بعد اسم الدالة بالشكل:<syntaxhighlight lang="kotlin"> | |||
val l = singletonList<Int>(1) | |||
</syntaxhighlight>وقد يُستغنى عنها إن كان بالإمكان تحديدها من السياق، كما في الشيفرة الصحيحة الآتية:<syntaxhighlight lang="kotlin"> | |||
val l = singletonList(1) | |||
</syntaxhighlight> | |||
== مصادر == | ==القيود المُعمَّمة (Generic Constraints)== | ||
* [https://kotlinlang.org/docs/reference/generics.html صفحة الأنواع المُعمَّمة في التوثيق الرسمي للغة Kotlin] | ==حذف الأنواع (Type Erasure)== | ||
==مصادر== | |||
*[https://kotlinlang.org/docs/reference/generics.html صفحة الأنواع المُعمَّمة في التوثيق الرسمي للغة Kotlin] | |||
[[تصنيف:Kotlin]] | [[تصنيف:Kotlin]] | ||
[[تصنيف:Kotlin Types]] | [[تصنيف:Kotlin Types]] | ||
[[تصنيف:Kotlin Java]] | [[تصنيف:Kotlin Java]] |
مراجعة 08:51، 14 مارس 2018
متحوِّلات الأنواع (Type Parameters)
قد تحتوي الأصناف (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) فهو أمرٌ آخر مختلف.
التغيُّر في موقع التصريح (Declaration-site Variance)
بفرض وجود الواجهة المُعمَّمة (generic interface) Source<T>
غير المحتوية على أيّ تابعٍ (method) له متحولاتٌ من النوع T
وإنما تحتوي فقط على توابعَ تعيد النوع T
، بالشكل:
// Java شيفرة
interface Source<T> {
T nextT();
}
فمن الآمن حينئذٍ تخزين مرجعيّةٍ (reference) لكائنٍ (instance) من Source<String>
في متحولٍ (variable) من النوع Source<Object>
حيث لا يوجد أيّ تابعٍ مستهلكٍ (consumer)، ولكن Java لا تسمح بذلك، كما في الشيفرة الآتية:
// Java شيفرة
void demo(Source<String> strs) {
Source<Object> objects = strs; // تعليمة غير مسموحة في Java
// ...
}
وللتغلُّب على تلك المشكلة يجب التصريح عن الكائنات من النوع Source<? extends Object>
، وهذا لا يبدو منطقيًا لأنه لا زال بالإمكان استدعاءُ كلُّ التوابع -نفسَها- كما في السابق، وبالتالي لا طائل من إضافة نوعٍ مُعقَّدٍ كهذا، ولكنّها الطريقة التي يعمل بها المُترجِم (compiler)!
أمّا لغة Kotlin فتدعم طريقةً أفضل تُدعَى "التغيُّر في موقع التصريح (declaration-site variance)" وذلك من خلال إضافة المُحدَّد (modifier) out
لمتحوِّل النوع T
من Source
وذلك لضمان كونه نوعًا مُعادًا فقط (أيّ مُنتَجًا [produced]) من عناصر Source<T>
لا مستهلَكًا (consumed)، ويتمِّ ذلك بالشكل:
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // هذا مسموح لوجود الكلمة المفتاحيّة out
// ...
}
القاعدة العامّة: إن عُرِّفَ متحول النوع T
من الصنف C
بالمُحدِّد out
فيُسمَح له أن يكون نوعًا "صادرًا" فقط عن عناصر C
، وبالمقابل من الآمن أن يكون C<Base>
نوعًا أعلى (supertype) من C<Derived>
، وبصياغةٍ أخرى: يُعدُّ الصنف C
من النوع covariant في المتحول T
أو إن T
متحولُ نوعٍ covariant، وخلاصة القول: يكون C
منتِجًا للنوع T
لا مستهلِكًا له.
ويأتي دعم لغة Kotlin للتغيُّر في موقع التصريح (declaration-site variance) عبر المُحدِّد out
على عكس ما في لغة Java ، والذي هو التغيُّر في استخدام التصريح (use-site variance) حيث تجعل توسعةُ الأنواع (wildcards) الأنواعَ covariant.
كما أنّ Kotlin تدعم المُحدِّد in
مما يجعل متحول النوع contravariant، أي أنّه من المُمكن أن يكون مستهلَكًا لا منتَجًا، ويُعدُّ النوع Comparable
خير مثالٍ عنه كما في الشيفرة الآتية:
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // القيمة 1.0 لها النوع Double
// وهو نوع فرعيّ من النوع Number
// وبالتالي يمكن إسناد المتحول x
// إلى متحول آخر من النوع Comparable<Double>
val y: Comparable<Double> = x // شيفرة صحيحة
}
وقد استُخدِم المُحدِّدان in
و out
في لغة C# لبعض الوقت.
الأنواع المُسقَطة (Type Projection)
التغيُّر في موقع الاستخدام (Use-site Variance): الأنواع المُسقَطة (Type Projection)
من السهل التصريحُ عن متحوِّل النوع T
كنوع out
وتجنُّب المشاكل الناتجة عن الأصناف الفرعيّة في مكان استخدامها، ولكن تقتصر بعض الأصناف على النوع T
كنوعٍ مُعادٍ، كما هو الحال في المصفوفات (Array) بالشيفرة الآتية:
class Array<T>(val size: Int) {
fun get(index: Int): T { /* ... */ }
fun set(index: Int, value: T) { /* ... */ }
}
فلا يُمكن أن يكون هذا الصنف من النوع covariant ولا contravariant (المشروحين سابقًا في هذه الصفحة) بالنسبة لمتحول النوع T
، وهذا بالمقابل لا يمنح مرونةً في البرمجة، فإن كانت الدالة الآتية:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
يتَّضِح من الشيفرة السابقة أنّ هذه الدالة تنسخ العناصر من مصفوفةٍ لأخرى، ولدى تجربتها واقعيًا:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any) // سينتج خطأ لأن من المتوقع أن تكون الأنواع
// (Array<Any>, Array<Any>)
ونعود للمشكلة الشائعة هنا؛ وهي أنّ Array<T>
ثابتة (invariant) في T
وبالتالي فليس أيٌّ من النوعين Array<Int>
و Array<Any>
صنفًا فرعيًا (subtype) للآخر، ولكن لِمَ؟ لأنّ عملية النسخ قد تجلب بعض المتاعب البرمجية مثل محاولة كتابة سلسلة نصيّة (string) في المصفوفة from
، وبالتالي إذا مُرِّرت مصفوفة أعدادٍ صحيحةٍ Int
هناك فقد ينتُج استثناء ClassCastException
فيما بعد.
ولذلك يجب ضمان أمرٍ واحدٍ وهو أنّ دالة copy()
لا تحتمِل حدوث أيّ مشكلة ويجب إذن منعها من الكتابة في from
كما في الشيفرة الآتية:
fun copy(from: Array<out Any>, to: Array<Any>) {
// ...
}
وما يحدث هنا يُسمّى بالأنواع المُسقَطة (type projection) بحيث يمكن القول بأن from
مصفوفةٌ لكنها مُسقَطةٌ محدودةٌ إذ يمكن استدعاء تلك التوابع (methods) التي تعيد متحوِّل النوع T
فقط، مما يعني أنّه بالإمكان الاستفادة من ()get
فقط، وهذه طريقة Kotlin في تغيُّر موقع الاستخدام (use-site variance) وهي تُماثل Array<? extends Object>
في Java ولكن بطريقة أبسط.
كما ويُتاح إسقاطُ نوعٍ ما باستخدام المعامل in
كما في الشيفرة الآتية:
fun fill(dest: Array<in String>, value: String) {
// ...
}
إذ توافق الشيفرةُ Array<in String>
الشيفرةَ Array<? super String>
في Java، أي يمكن تمريرُ مصفوفةٍ من CharSequence
أو مصفوفةٍ من Object
للدالة fill()
.
الإسقاط الواسع (Star-projection)
قد لا تتوافر -في بعض الأحيان- معلوماتٌ كافيةٌ عن متحوِّل النوع (type argument) ولا بُدَّ من استخدامه بشكل آمنٍ، يتمُّ ذلك عن طريق تعريف إسقاطٍ من النوع المُعمَّم (generic) بحيث يكون كل كائنٍ (instance) فعليٍّ من هذا النوع المُعمَّم نوعًا فرعيًا (subtype) من هذا الإسقاط.
وتوفِّر Kotlin صيغة الإسقاط الواسع لكلِّ من:
Foo<out T : TUpper>
إذ يكونT
نوعًا covariant عبر الحدِّ العلويTUpper
وتكون الصيغةFoo<*>
مكافئةً للصيغةFoo<out TUpper>
مما يعني أنّه عندما يكون النوعT
غير معروفٍ فمن الآمن قراءةُ قيمTUpper
منFoo<*>
.Foo<in T>
إذ يكونT
من النوع contravariant وتكون الصيغةFoo<*>
مكافئةً للصيغةFoo<in Nothing>
ممّا يعني أنه عندما يكون النوعT
غير معروفٍ فلا وجود لما يُكتب بطريقة آمنة فيFoo<*>
.Foo<T : TUpper>
إذ يكونT
نوعًا ثابتًا (invariant) عبر الحدِّ العلويTUpper
وتكون الصيغةFoo<*>
مكافئةً للصيغةFoo<out TUpper
> لقراءة القيم ومكافئةً للصيغةFoo<in Nothing>
لكتابتها.
ولدى وجود عدّة متحوِّلات للأنواع لنفس النوع المُعمَّم (generic) فيمكن حينئذٍ أن تُسقَط بشكلٍ مستقِّلٍ، فإن كان النوع مثلًا مُصرَّحًا بالشكل: interface Function<in T, out U>
تكون الإسقاطات الواسعة حينها بالشكل:
Function<*, String>
والتي تعنيFunction<in Nothing, String>
Function<Int, *>
والتي تعنيFunction<Int, out Any?>
Function<*, *>
والتي تعنيFunction<in Nothing, out Any?>
وهي تُماثِل بذلك الأنواع الخامّ في Java ولكنها آمنة (safe).
الدوال المُعمَّمة (Generic Functions)
قد يكون للدوال أيضًا متحولاتٌ للأنواع تُكتب قبل اسم الدالة بالشكل:
fun <T> singletonList(item: T): List<T> {
// ...
}
fun <T> T.basicToString() : String { // دالة إضافية extension
// ...
}
ولاستدعاء الدالة المُعمَّمة يجب تحديد متحولات النوع في الاستدعاء بعد اسم الدالة بالشكل:
val l = singletonList<Int>(1)
وقد يُستغنى عنها إن كان بالإمكان تحديدها من السياق، كما في الشيفرة الصحيحة الآتية:
val l = singletonList(1)