المجموعات (Collections): القائمة (List) والمجموعة (Set) و الخارطة (Map) في لغة Kotlin

من موسوعة حسوب

تُميِّز لغة Kotlin (على خلاف العديد من لغات البرمجة) بين المجموعة المتغيّرة (mutable) والثابتة (immutable)، وتساهم قدرتُها على التحكُّم الدقيق في الحالات التي يُسمَح فيها بتعديل المجموعات في التقليل من الأخطاء البرمجية (bugs) وتحقيق تصميمٍ أفضل لواجهات API.

ومن المهمِّ معرفة الفوارق ما بين العرض (view) الثابت للقراءة فقط (read-only) للمجموعة المتغيّرة والمجموعة الثابتة الفعليّة، إذ من السهل إنشاء كلِّ منهما ولكن نظام الأنواع في Kotlin لا يوضّح الفرق بينهما، ويقع ذلك على عاتق المتعلِّم.

يُعدُّ النوع List<out T>‎ في لغة Kotlin واجهةً (interface) تزوِّد بعمليات القراءة فقط مثل size و get وما شابه، وكما هو الحال في لغة Java فهي ترِث من النوع Collection<T>‎ والذي بدوره يرِث من Iterable<T>‎، أمّا التوابع (methods) التي تغيّر القائمة (list) فتُضاف عبر الواجهة MutableList<T>‎. ويشمل هذا النموذج أيضًا Set<out T>/MutableSet<T>‎ و Map<K, out V>/MutableMap<K, V>‎.

تبيّن الشيفرة الآتية الاستخدامات الأساسيّة لأنواع المجموعات:

val numbers: MutableList<Int> = mutableListOf(1, 2, 3)
val readOnlyView: List<Int> = numbers
println(numbers)        // ستظهر النتيجة
                        // "[1, 2, 3]"
numbers.add(4)
println(readOnlyView)   //  ستظهر النتيجة 
                        // "[1, 2, 3, 4]"
readOnlyView.clear()    // لن يُترجم
val strings = hashSetOf("a", "b", "c", "c")
assert(strings.size == 3)

ولا توجد في Kotlin بُنىً (structures) مخصَّصة لإنشاء القوائم (lists) أو المجموعات (sets)، بل تعتمد بذلك على التوابع الموجودة في المكتبة القياسيّة مثل: listOf()‎ و mutableListOf()‎ و setOf()‎ و mutableSetOf()‎ ، أما عند إنشاء المجموعة من النوع map فتُستخدَم الصيغة الاصطلاحيّة: mapOf(a to b, c to d)‎، وذلك في الشيفرات التي لا تكون حاسمة الأداء (NOT performance-critical). ويشير المتغيِّر من النوع readOnlyView لنفس القائمة والتغييرات مثل تغييرات القائمة الضمنية (underlying list changes)، فإذا كان نوع المرجعيّات الوحيدة الموجودة للقائمة من نوع القراءة فقط (read-only) فيمكن القول عن هذه المجموعة أنها ثابتةٌ، ومن السهل إنشاء مثل هذا النوع من خلال الشيفرة:

val items = listOf(1, 2, 3)

إذ يُعرَّف استخدام (implement) التابع listOf باستخدام قائمة مصفوفيّة (array list) في الوقت الحالي، ولكن سيعتمد النوع المُعاد -في المستقبل- على أنواع مجموعاتٍ غير متغيرةٍ وأكثر فعّالية من ناحية الذاكرة.

وتُعدُّ أنواع القراءة فقط من النوع covariant وهذا يعني بأنه من الممكن إسناد List<Rectangle>‎ إلى List<Shape>‎ (بفرض أنّ المستطيل [Rectangle] يرِث من الشكل [Shape])، ولا يُسمح بالقيام بهذا في أنواع المجموعات المتغيّرة لأنها ستؤدي إلى حدوث خللٍ أثناء التنفيذ.

وقد تحتاج في بعض الحالات للعودة إلى المُستدعي (caller) بلقطةٍ (snapshot) من المجموعة عند نقطة زمنيّة مُحدَّدة بحيث لا تتغير فيما بعد، مثل:

class Controller {
    private val _items = mutableListOf<String>()
    val items: List<String> get() = _items.toList()
}

إذ ينسخ التابع الإضافيّ toList عناصر القائمة وحسب، وبالتالي فإن القائمة المُعادَة لن تتغير. وهناك الكثير من التوابع الإضافيّة المفيدة في القوائم (lists) والمجموعات (sets)، مثل:

val items = listOf(1, 2, 3, 4)
items.first() == 1
items.last() == 4
items.filter { it % 2 == 0 }   // [2, 4] ستعيد

val rwList = mutableListOf(1, 2, 3)
rwList.requireNoNulls()        // [1, 2, 3] ستعيد
if (rwList.none { it > 6 }) println("No items above 6")  // ستطبع "No items above 6"
val item = rwList.firstOrNull()

بالإضافة إلى بعض الأدوات المساعدة مثل sort (ترتيب) و zip (ضغط) وfold (طيّ) و reduce (تخفيض) وغيرها. وتتبع map لنفس النموذج حيث من السهل إنشاؤها والوصول إليها كما هو موضح بالشيفرة:

val readWriteMap = hashMapOf("foo" to 1, "bar" to 2)
println(readWriteMap["foo"])  // "1" ستطبع
val snapshot: Map<String, Int> = HashMap(readWriteMap)

مصادر