الفرق بين المراجعتين لصفحة: «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- على | |||
class Box<T>(t: T) { | class Box<T>(t: T) { | ||
var value = t | var value = t | ||
سطر 8: | سطر 7: | ||
</syntaxhighlight>ولإنشاء كائنٍ (instance) من هذا الصنف يجب تحديد النوع كما في الشيفرة الآتية:<syntaxhighlight lang="kotlin"> | </syntaxhighlight>ولإنشاء كائنٍ (instance) من هذا الصنف يجب تحديد النوع كما في الشيفرة الآتية:<syntaxhighlight lang="kotlin"> | ||
val box: Box<Int> = Box<Int>(1) | val box: Box<Int> = Box<Int>(1) | ||
</syntaxhighlight>أمّا إن كان بالإمكان معرفة (infer) | </syntaxhighlight>أمّا إن كان بالإمكان معرفة (infer) المعاملات إمّا من خلال معاملات الباني (constructor arguments) أو بأيّ وسيلةٍ أخرى فيمكن حينئذٍ حذف نوع المعاملات، مثل:<syntaxhighlight lang="kotlin"> | ||
val box = Box(1) // القيمة 1 لها نوع الأعداد الصحيحة | val box = Box(1) // القيمة 1 لها نوع الأعداد الصحيحة | ||
// وبالتالي فإن المترجم سيحدد تلقائيًا النوع | // وبالتالي فإن المترجم سيحدد تلقائيًا النوع | ||
سطر 44: | سطر 43: | ||
void addAll(Collection<? extends E> items); | void addAll(Collection<? extends E> items); | ||
} | } | ||
</syntaxhighlight>إن | </syntaxhighlight>إن وسيط (argument) النوع المُوسَّع <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> | والسبيل لفهم آلية العمل بمثل هذه الحالات بسيط؛ إن كان بإلإمكان الحصول فقط على العناصر من مجموعة (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>. | ||
وتُسمَّى الكائنات المُتاحة للقراءة فقط بالمُنتِجات (Producers) وتلك المتاحة للكتابة بالمستهلكات (Consumers)، وبتعبيرٍ أفضل: Producer-Extends, Consumer-Super. | وتُسمَّى الكائنات المُتاحة للقراءة فقط بالمُنتِجات (Producers) وتلك المتاحة للكتابة بالمستهلكات (Consumers)، وبتعبيرٍ أفضل: Producer-Extends, Consumer-Super. | ||
<span> </span>لاحظ أنّه عند استخدام الكائن المنتِج وليكن <code>List<? extends Foo></code> فلا يمكن استدعاء التابع<code>add()</code> أو <code>set()</code> عبر هذا الكائن، ولكن ذلك لا يعني أنّ هذا الكائن ثابتٌ فلا يوجد ما يمنع استدعاء <code>clear()</code> لإزالة كلِّ العناصر من القائمة، وذلك لأن الدالة <code>clear()</code> لا تتطلَّب وجود أيّة | <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) له | بفرض وجود الواجهة المُعمَّمة (generic interface) <code>Source<T></code> غير المحتوية على أيّ تابعٍ (method) له معاملات من النوع <code>T</code> وإنما تحتوي فقط على توابعَ تعيد النوع <code>T</code>، بالشكل:<syntaxhighlight lang="java"> | ||
// Java شيفرة | // Java شيفرة | ||
interface Source<T> { | interface Source<T> { | ||
T nextT(); | T nextT(); | ||
} | } | ||
</syntaxhighlight>فمن الآمن حينئذٍ تخزين مرجعيّةٍ (reference) لكائنٍ (instance) من <code>Source<String></code> في | </syntaxhighlight>فمن الآمن حينئذٍ تخزين مرجعيّةٍ (reference) لكائنٍ (instance) من <code>Source<String></code> في متغيِّر (variable) من النوع <code>Source<Object></code>حيث لا يوجد أيّ تابعٍ مستهلكٍ (consumer)، ولكن Java لا تسمح بذلك، كما في الشيفرة الآتية:<syntaxhighlight lang="java"> | ||
// Java شيفرة | // Java شيفرة | ||
void demo(Source<String> strs) { | void demo(Source<String> strs) { | ||
سطر 65: | سطر 64: | ||
</syntaxhighlight>وللتغلُّب على تلك المشكلة يجب التصريح عن الكائنات من النوع <code>Source<? extends Object></code>، وهذا لا يبدو منطقيًا لأنه لا زال بالإمكان استدعاءُ كلُّ التوابع -نفسَها- كما في السابق، وبالتالي لا طائل من إضافة نوعٍ مُعقَّدٍ كهذا، ولكنّها الطريقة التي يعمل بها المُترجِم (compiler)! | </syntaxhighlight>وللتغلُّب على تلك المشكلة يجب التصريح عن الكائنات من النوع <code>Source<? extends Object></code>، وهذا لا يبدو منطقيًا لأنه لا زال بالإمكان استدعاءُ كلُّ التوابع -نفسَها- كما في السابق، وبالتالي لا طائل من إضافة نوعٍ مُعقَّدٍ كهذا، ولكنّها الطريقة التي يعمل بها المُترجِم (compiler)! | ||
أمّا لغة Kotlin فتدعم طريقةً أفضل تُدعَى "التغيُّر في موقع التصريح (declaration-site variance)" وذلك من خلال إضافة المُحدَّد (modifier) <code>out</code> | أمّا لغة Kotlin فتدعم طريقةً أفضل تُدعَى "التغيُّر في موقع التصريح (declaration-site variance)" وذلك من خلال إضافة المُحدَّد (modifier) <code>out</code> لمعامل النوع <code>T</code> من <code>Source</code> وذلك لضمان كونه نوعًا مُعادًا فقط (أيّ مُنتَجًا [produced]) من عناصر <code>Source<T></code> لا مستهلَكًا (consumed)، ويتمِّ ذلك بالشكل:<syntaxhighlight lang="kotlin"> | ||
interface Source<out T> { | interface Source<out T> { | ||
fun nextT(): T | fun nextT(): T | ||
سطر 74: | سطر 73: | ||
// ... | // ... | ||
} | } | ||
</syntaxhighlight>'''القاعدة العامّة:''' إن عُرِّفَ | </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) الأنواعَ covariant. | ويأتي دعم لغة Kotlin للتغيُّر في موقع التصريح (declaration-site variance) عبر المُحدِّد <code>out</code> على عكس ما في لغة Java ، والذي هو التغيُّر في استخدام التصريح (use-site variance) حيث تجعل توسعةُ الأنواع (wildcards) الأنواعَ covariant. | ||
كما أنّ Kotlin تدعم المُحدِّد <code>in</code> مما يجعل | كما أنّ Kotlin تدعم المُحدِّد <code>in</code> مما يجعل معامل النوع contravariant، أي أنّه من المُمكن أن يكون مستهلَكًا لا منتَجًا، ويُعدُّ النوع <code>Comparable</code> خير مثالٍ عنه كما في الشيفرة الآتية:<syntaxhighlight lang="kotlin"> | ||
interface Comparable<in T> { | interface Comparable<in T> { | ||
operator fun compareTo(other: T): Int | operator fun compareTo(other: T): Int | ||
سطر 86: | سطر 85: | ||
x.compareTo(1.0) // القيمة 1.0 لها النوع Double | x.compareTo(1.0) // القيمة 1.0 لها النوع Double | ||
// وهو نوع فرعيّ من النوع Number | // وهو نوع فرعيّ من النوع Number | ||
// وبالتالي يمكن إسناد | // وبالتالي يمكن إسناد المعامل x | ||
// إلى | // إلى متغيِّر آخر من النوع Comparable<Double> | ||
val y: Comparable<Double> = x // شيفرة صحيحة | val y: Comparable<Double> = x // شيفرة صحيحة | ||
سطر 95: | سطر 94: | ||
=== التغيُّر في موقع الاستخدام (Use-site Variance): الأنواع المُسقَطة (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) { | class Array<T>(val size: Int) { | ||
fun get(index: Int): T { /* ... */ } | fun get(index: Int): T { /* ... */ } | ||
fun set(index: Int, value: T) { /* ... */ } | fun set(index: Int, value: T) { /* ... */ } | ||
} | } | ||
</syntaxhighlight>فلا يُمكن أن يكون هذا الصنف من النوع covariant ولا contravariant (المشروحين سابقًا في هذه الصفحة) بالنسبة | </syntaxhighlight>فلا يُمكن أن يكون هذا الصنف من النوع covariant ولا contravariant (المشروحين سابقًا في هذه الصفحة) بالنسبة لمعامل النوع <code>T</code>، وهذا بالمقابل لا يمنح مرونةً في البرمجة، فإن كانت الدالة الآتية:<syntaxhighlight lang="kotlin"> | ||
fun copy(from: Array<Any>, to: Array<Any>) { | fun copy(from: Array<Any>, to: Array<Any>) { | ||
assert(from.size == to.size) | assert(from.size == to.size) | ||
سطر 117: | سطر 116: | ||
// ... | // ... | ||
} | } | ||
</syntaxhighlight>وما يحدث هنا يُسمّى بالأنواع المُسقَطة (type projection) بحيث يمكن القول بأن <code>from</code> مصفوفةٌ لكنها مُسقَطةٌ محدودةٌ إذ يمكن استدعاء تلك التوابع (methods) التي تعيد | </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"> | كما ويُتاح إسقاطُ نوعٍ ما باستخدام المعامل <code>in</code> كما في الشيفرة الآتية:<syntaxhighlight lang="kotlin"> | ||
سطر 126: | سطر 125: | ||
=== الإسقاط الواسع (Star-projection) === | === الإسقاط الواسع (Star-projection) === | ||
قد لا تتوافر -في بعض الأحيان- معلوماتٌ كافيةٌ عن | قد لا تتوافر -في بعض الأحيان- معلوماتٌ كافيةٌ عن وسيط النوع (type argument) ولا بُدَّ من استخدامه بشكل آمنٍ، يتمُّ ذلك عن طريق تعريف إسقاطٍ من النوع المُعمَّم (generic) بحيث يكون كل كائنٍ (instance) فعليٍّ من هذا النوع المُعمَّم نوعًا فرعيًا (subtype) من هذا الإسقاط. | ||
وتوفِّر Kotlin صيغة الإسقاط الواسع لكلِّ من: | وتوفِّر Kotlin صيغة الإسقاط الواسع لكلِّ من: | ||
سطر 132: | سطر 131: | ||
* <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<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> لكتابتها. | * <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<*, String></code> والتي تعني <code>Function<in Nothing, String></code> | ||
* <code>Function<Int, *></code> والتي تعني <code>Function<Int, out Any?></code> | * <code>Function<Int, *></code> والتي تعني <code>Function<Int, out Any?></code> | ||
سطر 139: | سطر 138: | ||
==الدوال المُعمَّمة (Generic Functions)== | ==الدوال المُعمَّمة (Generic Functions)== | ||
قد يكون للدوال أيضًا | قد يكون للدوال أيضًا معاملات للأنواع تُكتب قبل اسم الدالة بالشكل:<syntaxhighlight lang="kotlin"> | ||
fun <T> singletonList(item: T): List<T> { | fun <T> singletonList(item: T): List<T> { | ||
// ... | // ... | ||
سطر 147: | سطر 146: | ||
// ... | // ... | ||
} | } | ||
</syntaxhighlight>ولاستدعاء الدالة المُعمَّمة يجب تحديد | </syntaxhighlight>ولاستدعاء الدالة المُعمَّمة يجب تحديد معاملات النوع في الاستدعاء بعد اسم الدالة بالشكل:<syntaxhighlight lang="kotlin"> | ||
val l = singletonList<Int>(1) | val l = singletonList<Int>(1) | ||
</syntaxhighlight>وقد يُستغنى عنها إن كان بالإمكان تحديدها من السياق، كما في الشيفرة الصحيحة الآتية:<syntaxhighlight lang="kotlin"> | </syntaxhighlight>وقد يُستغنى عنها إن كان بالإمكان تحديدها من السياق، كما في الشيفرة الصحيحة الآتية:<syntaxhighlight lang="kotlin"> | ||
سطر 154: | سطر 153: | ||
==قيود الأنواع المُعمَّمة (Generic Constraints)== | ==قيود الأنواع المُعمَّمة (Generic Constraints)== | ||
تحدُّ هذه القيود من خيارات مجموعة الأنواع المُتاحة كبديلٍ | تحدُّ هذه القيود من خيارات مجموعة الأنواع المُتاحة كبديلٍ لمعامل النوع الحاليّ. | ||
=== قيد الحدود العليا (Upper Bounds) === | === قيد الحدود العليا (Upper Bounds) === | ||
سطر 161: | سطر 160: | ||
// ... | // ... | ||
} | } | ||
</syntaxhighlight>إذ يكون الحدُّ الأعلى هو النوع المُحدَّد بعد النقطتين الرأسيّتين (<code>:</code>)، ويمكن استبدال الأنواع الفرعيّة فقط من <code>Comparable<T></code> | </syntaxhighlight>إذ يكون الحدُّ الأعلى هو النوع المُحدَّد بعد النقطتين الرأسيّتين (<code>:</code>)، ويمكن استبدال الأنواع الفرعيّة فقط من <code>Comparable<T></code> بمعامل النوع <code>T</code> مثل:<syntaxhighlight lang="kotlin"> | ||
sort(listOf(1, 2, 3)) // تعليمة صحيحة لأن نوع الأعداد الصحيحة Int | sort(listOf(1, 2, 3)) // تعليمة صحيحة لأن نوع الأعداد الصحيحة Int | ||
// هو نوع فرعي من Comparable<Int> | // هو نوع فرعي من Comparable<Int> | ||
سطر 178: | سطر 177: | ||
==إزالة الأنواع (Type Erasure)== | ==إزالة الأنواع (Type Erasure)== | ||
تجري عمليات التحقُّق بهدف ضمان سلامة الأنواع (والتي تجريها لغة Kotlin على استخدام تصريحات الأنواع المُعمَّمة [generics]) أثناء عملية الترجمة (compilation) فقط، أما أثناء التنفيذ (runtime) فإن الكائنات (instances) من الأنواع المُعمَّمة لا تحتوي على أيّة معلوماتٍ تدلُّ على | تجري عمليات التحقُّق بهدف ضمان سلامة الأنواع (والتي تجريها لغة Kotlin على استخدام تصريحات الأنواع المُعمَّمة [generics]) أثناء عملية الترجمة (compilation) فقط، أما أثناء التنفيذ (runtime) فإن الكائنات (instances) من الأنواع المُعمَّمة لا تحتوي على أيّة معلوماتٍ تدلُّ على معاملات أنواعها الفعليّة، ويُصطلح على أنها قد أُزيلت (erased)؛ لذلك فإن الكائنات من النوع <code>Foo<Bar></code> والنوع <code>Foo<Baz?></code> مثلًا تُزال لتصبح فقط من <code>Foo<*></code>. | ||
وبالتالي فإنّه ما من طريقةٍ عامّةٍ للتحقُّق فيما إذا أُنشِئ الكائن من النوع المُعمَّم أثناء التنفيذ، ويَمنع المُترجِم [[Kotlin/typecasts|عمليات التحقُّق (باستخدام <code>is</code>) هذه]]، ولا يمكن التحقق كذلك -أثناء التنفيذ- من عمليات التحويل إلى الأنواع المعمَّمة | وبالتالي فإنّه ما من طريقةٍ عامّةٍ للتحقُّق فيما إذا أُنشِئ الكائن من النوع المُعمَّم أثناء التنفيذ، ويَمنع المُترجِم [[Kotlin/typecasts|عمليات التحقُّق (باستخدام <code>is</code>) هذه]]، ولا يمكن التحقق كذلك -أثناء التنفيذ- من عمليات التحويل إلى الأنواع المعمَّمة بمعاملات أنواع حقيقيّة (concrete) (مثل <code>foo as List<String></code>)، وتُستخدَم هذه التحويلات (غير المُتحقَّق منها) عندما يكون أمان النوع (type safety) موجودًا ضمنيًا في منطق البرنامج عالي المستوى (high-level) ولا يمكن للمترجم معرفته (infer)، إذ سيُظهِر المترجم تحذيرًا بمثل هذه التحويلات (غير المُتحقَّق منها) لأن عمليات التحقُّق أثناء التنفيذ تشمل الأجزاءَ غير المُعمَّمة فقط (ما يكافِئ <code>foo as List<*></code>). | ||
وتتمُّ أثناء الترجمة (لا التنفيذ) عملياتُ التحقُّق من | وتتمُّ أثناء الترجمة (لا التنفيذ) عملياتُ التحقُّق من معاملات الأنواع لاستدعاءات الدوال، إذ ليس بالإمكان استخدام معامل النوع للتحقُّق من النوع داخل بنية الدالة (function body)، كما ولا يُتحقَّق من تحويلات الأنواع (typecasts) لمعاملات الأنواع (مثل <code>foo as T</code>)، أمّا معاملات النوع reified في الدوال المباشرة فتُستبدَل ليحِلّ محلها معامل النوع الفعليّ في بنية الدالة (function body) عند نقطة الاستدعاء، ويُمكن إذن استخدامُها بعمليات التحقُّق والتحويل بين الأنواع (casts) وبنفس القيود على كائنات (instances) الأنواع المُعمَّمة كما وُصِف سابقًا. | ||
==مصادر== | ==مصادر== |
المراجعة الحالية بتاريخ 16:05، 4 يوليو 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);
}
إن وسيط (argument) النوع المُوسَّع ? 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)
قيود الأنواع المُعمَّمة (Generic Constraints)
تحدُّ هذه القيود من خيارات مجموعة الأنواع المُتاحة كبديلٍ لمعامل النوع الحاليّ.
قيد الحدود العليا (Upper Bounds)
وهو من أشهر القيود التي توافق الكلمة المفتاحيّة extend
في لغة Java كما في الشيفرة:
fun <T : Comparable<T>> sort(list: List<T>) {
// ...
}
إذ يكون الحدُّ الأعلى هو النوع المُحدَّد بعد النقطتين الرأسيّتين (:
)، ويمكن استبدال الأنواع الفرعيّة فقط من Comparable<T>
بمعامل النوع T
مثل:
sort(listOf(1, 2, 3)) // تعليمة صحيحة لأن نوع الأعداد الصحيحة Int
// هو نوع فرعي من Comparable<Int>
sort(listOf(HashMap<Int, String>())) // استخدام خاطئ لأنّ
// HashMap<Int, String>
// ليست نوعًا فرعيًا من
// Comparable<HashMap<Int, String>>
ويكون النوع Any?
الحدَّ العلويَّ الافتراضيّ ما لم يُحدَّد غيره، ولا يُسمَح بالتقييد بأكثر من حدٍّ واحدٍ، إذ تُستخدَم عبارة where
عندما يتطلَّب الأمر وجودَ أكثر من حدٍّ علويٍّ واحدٍ، مثل:
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
إزالة الأنواع (Type Erasure)
تجري عمليات التحقُّق بهدف ضمان سلامة الأنواع (والتي تجريها لغة Kotlin على استخدام تصريحات الأنواع المُعمَّمة [generics]) أثناء عملية الترجمة (compilation) فقط، أما أثناء التنفيذ (runtime) فإن الكائنات (instances) من الأنواع المُعمَّمة لا تحتوي على أيّة معلوماتٍ تدلُّ على معاملات أنواعها الفعليّة، ويُصطلح على أنها قد أُزيلت (erased)؛ لذلك فإن الكائنات من النوع Foo<Bar>
والنوع Foo<Baz?>
مثلًا تُزال لتصبح فقط من Foo<*>
.
وبالتالي فإنّه ما من طريقةٍ عامّةٍ للتحقُّق فيما إذا أُنشِئ الكائن من النوع المُعمَّم أثناء التنفيذ، ويَمنع المُترجِم عمليات التحقُّق (باستخدام is
) هذه، ولا يمكن التحقق كذلك -أثناء التنفيذ- من عمليات التحويل إلى الأنواع المعمَّمة بمعاملات أنواع حقيقيّة (concrete) (مثل foo as List<String>
)، وتُستخدَم هذه التحويلات (غير المُتحقَّق منها) عندما يكون أمان النوع (type safety) موجودًا ضمنيًا في منطق البرنامج عالي المستوى (high-level) ولا يمكن للمترجم معرفته (infer)، إذ سيُظهِر المترجم تحذيرًا بمثل هذه التحويلات (غير المُتحقَّق منها) لأن عمليات التحقُّق أثناء التنفيذ تشمل الأجزاءَ غير المُعمَّمة فقط (ما يكافِئ foo as List<*>
).
وتتمُّ أثناء الترجمة (لا التنفيذ) عملياتُ التحقُّق من معاملات الأنواع لاستدعاءات الدوال، إذ ليس بالإمكان استخدام معامل النوع للتحقُّق من النوع داخل بنية الدالة (function body)، كما ولا يُتحقَّق من تحويلات الأنواع (typecasts) لمعاملات الأنواع (مثل foo as T
)، أمّا معاملات النوع reified في الدوال المباشرة فتُستبدَل ليحِلّ محلها معامل النوع الفعليّ في بنية الدالة (function body) عند نقطة الاستدعاء، ويُمكن إذن استخدامُها بعمليات التحقُّق والتحويل بين الأنواع (casts) وبنفس القيود على كائنات (instances) الأنواع المُعمَّمة كما وُصِف سابقًا.