أستمع الى المقال

شرحنا في دروس سابقة، التجميعات Collections في لغة البرمجة كوتلن، وهي: تجميعة القوائم Lists و الخرائط Maps و الأطقم Sets. وتعلّمنا أيضًا، كيفية إجراء عمليات على تجميعة القوائم Lists. مثل: عمليات استعادة البيانات Retrieving، والتصفية Filtering والتحويل Transformation وغيرها، عبر استخدام دوال من مكتبة كوتلن القياسية.

ولكن، في بعض الأحيان، يكون من الأفضل وضع هذه البيانات في تجميعة الخرائط Maps. لأنه يتم تنظيم البيانات فيها، عبر أزواج من المفاتيح Keys والقيِّم Values. بالتالي توفر وصول أسهل وأسرع لقيم هذه البيانات، عبر استخدام مفاتيحها.

لذلك في هذا الدرس، سنتعلّم طرقاً مختلفة لبناء تجميعات خرائط، عبر استخدام دوال من مكتبة كوتلن. من هذه الطرق، عمليات الربط Association، والتجميع Grouping.

الربط Association في لغة كوتلن:

تسمح عمليات الربط، ببناء تجميعات خرائط من عناصر تجميعة ما وقيم معينة مرتبطة بها. في أنواع الربط المختلفة، يمكن أن تكون هذه العناصر أو جزء منها، إمّا مفاتيح أو قيمًا في الخريطة الناتجة من عملية الربط.

بعبارة أخرى، أنه إذا كان لدينا تجميعة قائمة List بها عناصر، يمكن أن نضع هذه العناصر في تجميعة الخريطة Map. ولأن عناصر تجميعة الخرائط تتكون من مفتاح وقيمة، يمكننا أن نضع عناصر القائمة List، كمفاتيح مرتبطة بقيمة ما، في تجميعة الخريطة الجديدة الناتجة من عملية الربط. أو جعل هذه العناصر هي القيمة، وربطها بمفتاح نختاره لها. أو يمكننا أخذ جزء من هذه العناصر، ووضعه كقيمة أو مفتاح.

ولفهم هذا الأمر أكثر، دعونا نستخدم الدوال التي تمكننا من القيام بعمليات الربط هذه. وهي: ()associateWith، و ()associateBy، و ()associate.

بناء الخرائط عبر دالة ()associateWith:

تُنشئ خريطة تكون فيها عناصر التجميعة الأصلية هي المفاتيح، أمّا القيّم في الخريطة، فسيتم تحديدها عن طريق تعبير اللامبدا أو تعبير المرجع المرسل إلى الدالة.

من المهم أن نعرف، أنه في حالة تساوي قيمة عنصرين، يتم الإبقاء على العنصر الأخير فقط كمفتاح في الخريطة. لأن مفاتيح الخريطة يجب أن تكون فريدة وغير مكررة:

val numbers = listOf("one", "two", "three", "four")
val newMap: Map<String, Int> = numbers.associateWith { it.length }

لدينا قائمة numbers تحتوي على عناصر نصية String. لتحويل هذه القائمة إلى تجميعة خرائط، استخدمنا معها دالة ()associateWith، وأرسلنا لها تعبير اللامبدا:

{ it.length }

الكلمة it تمثل عنصر واحد في كل مرة يتم الوصول إلى عناصر القائمة التي تستدعي الدالة حتى آخر عنصر فيها. ولأن عناصر القائمة هي من النوع String، يمكننا أن نستخدم معها الخاصيّة length والتي ستعيد لنا طول (عدد محارف) النص.

ففي المرة الأولى، تمثل الكلمة it العنصر الأول في قائمة numbers، وهو “one”. وعند استخدام length مع هذا العنصر، ستكون النتيجة هي الرقم 3، وهو طول نص العنصر.

في الشفرة أعلاه، ستضع الدالة ()associateWith عناصر القائمة كمفاتيح في الخريطة الجديدة، ثم تضع طول كل عنصر كقيمة لهذا العنصر المفتاح. وهذا يعني، أن نوع بيانات المفتاح هو نفس نوع بيانات العنصر في القائمة numbers، النوع String. ونوع بيانات القيمة، هو نفس نوع بيانات النتيجة العائدة من تعبير اللامبدا، النوع عدد صحيح Int.

لذلك، يكون نوع بيانات المتغير newMap والذي سيتم إسناد الخريطة الجديدة التي تعيدها دالة ()associateWith إليه:

Map<String, Int>

أي أن الخريطة التي سيُمثلها هذا المتغير، هي خريطة تكون مفاتيحها من النوع String، وقيمها من النوع Int. وإذا وضعنا المتغير في دالة ()println لطباعة قيمته:

println(newMap)

ستكون نتيجة الطباعة هي الخريطة Map التالية:

{one=3, two=3, three=5, four=4}

العنصر one من قائمة numbers، أصبح مفتاح key في خريطة newMap، وقيمة value هذا المفتاح هي طول أو عدد حروفه الذي يساوي 3. وهكذا تم تحويل كل عناصر القائمة الباقية.

عناصر قائمة List مكررة:

نلاحظ أن عناصر القائمة numbers غير متساوية القيمة، لذلك تم وضعها كلها كمفاتيح في الخريطة الجديدة دون حذف أيًا منها. ولكن ماذا لو كانت العناصر تحمل قيمة مكررة؟ في هذه الحالة سيتم الاحتفاظ بآخر عنصر في القائمة فقط:

val numbers = listOf("one", "two", "three", "three", "four")

تكرر العنصر “three” في القائمة numbers. وعند استدعاء دالة ()associateWith على القائمة:

numbers.associateWith { it.length }

ستكون الخريطة الناتجة هي:

{one=3, two=3, three=5, four=4}

تجاهلت الدالة القيمة three المكررة، لأن مفاتيح الخريطة، يجب أن تكون فريدة.

بناء الخرائط عبر دالة ()associateBy:

عند استخدام هذه الدالة مع قائمةٍ ما، فستكون النتيجة هي عكس ما تنتجه دالة ()associateWith. أي أن عناصر القائمة، هي من ستكون القيّم في الخريطة الجديدة، وستكون المفاتيح هي نتيجة تعبير اللامبدا:

fun main() {
    val numbers = listOf("one", "two","three", "four")
    val newMap = numbers.associateBy { it.first() }

    println(newMap)
}

في الشفرة أعلاه، ستعيد دالة ()first، أول حرف من كل عنصر قائمة numbers. وسيتم وضع هذا الحرف عبر دالة ()associateBy، كمفتاح في الخريطة الجديدة:

{o=one, t=three, f=four}

نلاحظ إختفاء العنصر الثاني “two” المتواجد في القائمة، من الخريطة الناتجة. لأن العنصرين “two” و “three”، لديهما نفس الحرف الأول t. ولأن المفاتيح يجب أن تكون فريدة كما قلنا سابقًا، لا يمكن فعل التالي:

{o=one, t=two, t=three, f=four}

تجاهلت الدالة العنصر “two”، لأنها وجدت أن هناك عنصرًا بعده لديه نفس قيمة المفتاح. ودائمًا سيتم الإبقاء على العنصر الأخير من العناصر التي تتشابه مفاتيحها.

بناء الخرائط عبر دالة ()associate:

يمكننا استخدام هذه الدالة، بدلًا عن استخدام الدالتين ()associateWith و ()associateBy. ولكن في هذه الحالة، يجب وضع المفاتيح والقيم معًا، مع الفصل بينهما بالكلمة to في تعبير اللامبدا:

كما نرى في الصورة، أن الدالة يمكنها ان تقوم بنفس الأمر الذي تقوم به الدالتين السابقتين. ونلاحظ أيضًا، أن برنامج IntellJ يضع خط أصفر متعرِّج تحت الكلمة associate. وعند وضع مؤشر الفأرة على الكلمة، ستظهر النافذة التالية:

يخبرنا البرنامج عبر هذه النافذة، أنه من الأفضل استبدال الدالة ()associate بالدالة ()associateBy. لماذا؟ حسنًا، لفهم ذلك دعونا نرى ما تخبرنا به الوثائق الرسمية لكوتلن عن هذه الدالة:

تنتج الدالة ()associate، كائنات أزواج Pair قصيرة الأجل والتي من الممكن أن تؤثر في أداء البرنامج. لذلك، يجب استخدام هذه الدالة، فقط عندما لا يكون الأداء مهمًا جدًا أو نفضل استخدامها أكثر من الخيارات (الدوال) الأخرى.

بناء الخرائط عبر دالة toMap:

نستخدم هذه الدالة، عندما يكون لدينا تجميعة عناصرها هي عبارة عن أزواج Pair. فمثلًا، إذا كان لدينا القائمة التالية:

val fruits = listOf(
    Pair("Orange", 15.0),
    Pair("Apple", 18.0),
    Pair("Avocado", 20.0)
)

كل عنصر من عناصر القائمة fruits، يتكون من كائنين: اسم الفاكهة من النوع String، وسعرها من النوع Int. لتحويل هذه القائمة إلى تجميعة خريطة Map، يمكننا استخدام دالة ()associateWith:

println(fruits.associateWith { it.first })

الكلمة it هنا تمثل كائن Pair والذي هو عنصر في القائمة. ويمكننا الوصول إلى الكائنين في كائن Pair، عبر الخاصيّتين: first للكائن الأول و second للكائن الثاني. إذًا التعبير:

{ it.first }

سينتج لنا قيمة الكائن الأول في كائن Pair. وهو الذي ستضعه الدالة ()associateWith كقيمة في تجميعة الخريطة، بينما سيكون المفتاح هو كائن الـ Pair نفسه:

{(Orange, 15.0)=Orange, (Apple, 18.0)=Apple, (Avocado, 20.0)=Avocado}

فإذا أخذنا العنصر الأول في تجميعة الخريطة الناتجة، نجد أن مفتاحه هو: (Orange, 15.0) بينما قيمته هي: Orange.

أمّا عند استخدام دالة ()associateBy:

println(fruits.associateBy { it.first })

ستكون نتيجة عناصر الخريطة، هي عكس الدالة السابقة:

{Orange=(Orange, 15.0), Apple=(Apple, 18.0), Avocado=(Avocado, 20.0)}

المفتاح هو Orange والقيمة هي (Orange, 15.0).

أمّا إذا كنا نريد وضع الكائن الأول في كائن الـ Pair هو المفتاح والكائن الثاني فيه هو القيمة، يمكننا استخدام دالة ()toMap، والتي تتوفر للقوائم التي تكون عناصرها من النوع Pair:

println(fruits.toMap())

وستكون نتيجة الطباعة هي:

{Orange=15.0, Apple=18.0, Avocado=20.0}

كما نرى، أصبحت الخريطة أكثر وضوحًا عند استخدام هذه الدالة.

ولكن إذا كنا نريد العكس، أي أن يكون الكائن الثاني هو المفتاح والكائن الأول هو القيمة، يمكننا استخدام دالة ()map، لتحويل وعكس عناصر الـ Pair أولًا، ثم استدعاء ()toMap:

println(fruits.map { it.second to it.first }.toMap())

وستكون نتيجة الطباعة هي:

{15.0=Orange, 18.0=Apple, 20.0=Avocado}

بناء الخرائط عبر دالة ()groupBy:

إذا كان لدينا قائمتين، إحداهما أسماء الفواكه والأخرى أسعارها:

val names = listOf("Orange", "Apple", "Banana", "Avocado", "Mango")
val prices = listOf(15.0, 18.0, 15.0, 18.0, 15.0)

يمكننا دمجهما في تجميعة جديدة، عبر استخدام دالة ()zip:

val fruits = names.zip(prices) { name, price -> 
    Fruits(name, price)
}

وستنتج لنا قائمة جديدة تتكون من عناصر القائمتين:

[
    Fruits(name=Orange, price=15.0), 
    Fruits(name=Apple, price=18.0), 
    Fruits(name=Banana, price=15.0), 
    Fruits(name=Avocado, price=18.0), 
    Fruits(name=Mango, price=15.0)
]

الآن يمكننا أن نستخدم البحث والتصفية لاستعادة بيانات معينة من القائمة الجديدة، حسب الشرط المُعطى. فمثلًا، إذا أردنا البحث عن كل الفواكه التي سعرها 15.0، يمكننا استخدام دالة ()filter والتي ستعيد قائمة جديدة فيها العناصر التي توافق الشرط فقط:

println(fruits.filter { it.price == 15.0 })

وستكون النتيجة:

[
    Fruits(name=Orange, price=15.0),
    Fruits(name=Banana, price=15.0),
    Fruits(name=Mango, price=15.0)
]

تم إحضار العناصر التي يكون سعرها 15.0 بالضبط. ثم للبحث عن العناصر التي يكون سعرها 18.0، يمكننا كتابة دالة ()filter أخرى:

println(fruits.filter { it.price == 18.0 })

وهكذا نفعل مع كل سعر جديد نريد البحث عن العناصر التي تملكه. نرى هنا أن هناك تكرار، وستنتج دالة ()filter قائمة جديدة في كل مرة نستخدمها. لذا يكون من الأفضل وضع السعر كمفتاح في تجميعة خرائط، لأن ذلك سيسهل علينا البحث ولن نحتاج لتكرار الشفرة، ولن يتم إنتاج قائمة جديدة في كل مرة نبحث عن العناصر.

للقيام بهذا الأمر، إحدى الطرق هي أن نستخدم دالة ()groupBy:

val groupedByPrice = fruits.groupBy(Fruits::price)

أرسلنا الشرط الذي يجب أن تتبعه الدالة في وضع المفتاح للقائمة التي تستدعيها، كتعبير مرجع. كان يمكننا أيضًا، إرسال الشرط كتعبير لامبدا:

val groupedByPrice = fruits.groupBy { it.price }

في كلا الحالتين، ستعيد الدالة تجميعة خرائط جديدة، يكون مفتاح العنصر فيها هو السعر price، وتكون قيمة العنصر، هي قائمة List بالعناصر التي توافق هذا المفتاح. سيتم إسناد التجميعة الجديدة، إلى المتغير groupedByPrice.

الآن كل ما نحتاجه هو استخدام أقواس الفهرسة المربعة [ ] أو دالة ()get  مع المتغير، ووضع المفتاح داخلها، لاستعادة القيم التي يمثلها. وبما أننا وضعنا السعر كمفتاح، يمكننا استخدامه في البحث:

println(groupedByPrice[15.0])

وستكون النتيجة هي نفس السابقة:

[
    Fruits(name=Orange, price=15.0), 
    Fruits(name=Banana, price=15.0), 
    Fruits(name=Mango, price=15.0)
]
println(groupedByPrice[18.0])
[
    Fruits(name=Apple, price=18.0), 
    Fruits(name=Avocado, price=18.0)
]

دوال استعادة أو إضافة قيم للخرائط:

لدينا بعض الدوال الأخرى المفيدة والتي يمكننا استخدامها مع تجميعة الخرائط. من هذه الدوال: ()getOrElse و ()getOrPut. وبجانب دالة ()filter التي رأيناها في الفقرات أعلاه، لدينا دالتي: ()filterKeys و ()filterValues.

دالة ()getOrElse:

هذه الدالة، ستحاول البحث عن القيمة التي تقابل المفتاح الذي يتم إرساله إليها. فإذا لم تجد المفتاح في التجميعة التي تستدعيها، ستعيد القيمة الافتراضية التي يجب أن نضعها لها. فمثلًا، إذا كان لدينا تجميعة الخرائط التالية:

val map = mapOf(1 to "one", 2 to "two")

عند استخدام هذه الدالة مع التجميعة أعلاه، يجب أن نضع قيمة افتراضية، ستعيدها الدالة إذا كانت نتيجة البحث هي null:

println(map.getOrElse(0) { "zero" })

نحاول البحث عن القيمة المرتبطة بالمفتاح 0. ولأن هذا المفتاح ليس متوفرًا في تجميعة map، سيتم طباعة “zero”. وهي القيمة الافتراضية التي وضعناها في تعبير اللامبدا الخاص بالدالة.

دالة ()getOrPut:

تعمل هذه الدالة مع تجمعيات الخرائط القابلة للتغيير MutableMap. إذا لم يكن المفتاح الذي يتم إرساله إليها، متوفرًا في التجميعة التي تستدعيها، ستضيف الدالة المفتاح والقيمة الافتراضية للتجميعة:

val mutableMap = mutableMapOf(1 to "one", 2 to "two")
mutableMap.getOrPut(0) { "zero" }
println(mutableMap)

وستكون نتيجة طباعة التجميعة بعد استخدام الدالة هي:

{1=one, 2=two, 0=zero}

تم إضافة المفتاح 0 والقيمة zero كعنصر في التجميعة.

دالة ()filterKeys:

ستطبق هذه الدالة الشرط المعطى لها، على مفاتيح تجميعة الخريطة، وتعيد خريطة جديدة بالعناصر التي توافق مفاتيحها الشرط:

val map = mapOf(1 to "one", 2 to "two", 3 to "three", 4 to "four")
println(map.filterKeys { it % 2 == 1 })

شرط اللامبدا أعلاه، هو للبحث عن المفاتيح في تجميعة map التي تكون أعداد فردية. ستكون نتيجة الطباعة، تجميعة خريطة جديدة:

{1=one, 3=three}

دالة ()filterValues:

أمّا عند استخدام هذه الدالة، فسيتم تطبيق الشرط المرسل إليها، على القيّم في التجميعة:

val map = mapOf(1 to "one", 2 to "two", 3 to "three", 4 to "four")
println(map.filterValues { it.contains('o') })

هنا نبحث عبر الشرط، عن أي قيمة في تجميعة map، تحتوي على الحرف o. ستكون نتيجة الطباعة، هي العناصر التي ينطبق الشرط على قيمها:

{1=one, 2=two, 4=four}

إجراء عمليات التحويل Transformation على الخرائط:

وهي بشكل رئيسي استخدام دالة التحويل ()map على تجميعة الخرائط Maps. نلاحظ أننا نستخدم الكلمة map، لتقديم فكرتين مختلفتين:

  • عند إجراء عمليات التحويل على التجميعات عبر دالة ()map.
  • أو عند تمثيل البيانات في شكل مفتاح key وقيمة value، والتي تمثلها تجميعة الخرائط Maps.

بعد معرفة الفرق بين الفكرتين، دعونا نرى كيفية استخدام دالة التحويل ()map وبعض الدوال الأخرى مثل: ()mapKeys و ()mapValues، على تجميعات الخرائط Maps.

دالة ()map:

عند استخدام هذه الدالة مع أي تجميعة، تعيد تجميعة من النوع قائمة List. فمثلًا، إذا كان لدينا تجميعة الخريطة التالية:

val numbersMap = mapOf(2 to "two", 4 to "four")
println(numbersMap)

ستكون نتيجة طباعتها هي:

{2=two, 4=four}

ولكن بعد إجراء التحويل لعناصر التجميعة عبر استخدام دالة ()map:

val numbersList = numbersMap.map {
    it.key to it.value
}

println(numbersList)

ستحول الدالة عناصر تجميعة الخرائط من مفاتيح وقيم، إلى أزواج في كائن Pair. كائنات الـ Pair هذه يتم وضعها في تجميعة جديدة من النوع List. وهي التي سيتم إسنادها للمتغير numbersList. لذا نتيجة طباعة المتغير ستكون:

[(2, two), (4, four)]

أمّا إذا أردنا تكون النتيجة هي تجميعة من النوع Map، فيمكننا استخدام دالة ()toMap، على النتيجة العائدة من دالة ()map:

println(numbersList.toMap())

يمكننا أيضًا بدلًا من استخدام الكلمة it، الوصول إلى عناصر التجميعة، باستخدام طريقة تفكيك التصريحات destructuring declaration:

val numbersList = numbersMap.map { (key, value) ->
    key to value
}

وستكون نتيجة عمل الدالة هي نفسها.

دالتي ()mapKeys و ()mapValues:

خلافًا للدالة السابقة، تعيد هاتان الدالتان تجميعة من النوع Map مباشرةً:

val numbersMap = mapOf(2 to "two", 4 to "four")
println(
    numbersMap.mapKeys { (key, _) -> -key }
        .mapValues { (_, value) -> "minus $value" }
)

نتيجة تنفيذ الشفرة أعلاه، هي:

{-2=minus two, -4=minus four}

لدى كل Map معاملين key و value يمكننا وضعهما في تعبير اللامبدا. فإذا وضعنا أحدهما ولم نحتاج لوضع الآخر، يمكننا استبداله بالشرطة السفلية، كما يظهر في الشفرة أعلاه، حتى نتجنب حدوث أخطاء، او قد يرفض المترجم Compiler تنفيذ الشفرة.

دوال أخرى من مكتبة كوتلن:

بعض الدوال الأخرى التي يمكننا استخدامها مع تجميعة الـ Map، مثل: ()any و ()all. تعيد الدالتين النتيجة true أو false، حسب الشرط المرسل إليها:

val map = mapOf(1 to "one", -2 to "minus two")

استخدام دالة ()any مع هذه التجميعة:

map.any { (key, _) -> key < 0 }

الشرط هو معرفة ما إذا كان هناك مفتاح أصغر  من 0 في تجميعة map. ستكون النتيجة true، لأن هناك مفتاح قيمته 2-.

استخدام دالة ()all:

map.all { (key, _) -> key < 0 }

الشرط هو معرفة ما إذا كان كل مفاتيح التجميعة أصغر  من 0. ستكون النتيجة false، لأن هناك مفتاح قيمته 1 وهو أكبر من الصفر بالطبع.

الخلاصة:

تعلمنا في هذا الدرس، العديد من الطرق في كيفية بناء الخرائط من التجميعة List، أو عبر استخدام دوال مباشرة. وتعلمنا أيضًا، كيفية استغلال الخرائط، في إسراع عمليات البحث عن البيانات، وإجراء العمليات المختلفة عليها.

بالطبع الدوال التي استخدمناها في هذا الدرس، ليست هي كلها ما يتوفر لتجميعة الخرائط Maps في مكتبة كوتلن، ولكن هذه هي أهمها. في الدروس القادمة، سندرس المزيد عن هذه التجميعة، وكيفية استغلالها لتنظيم بياناتنا في برامجنا.

هذا الدرس هو جزء من سلسلة تعليم مبادئ البرمجة بلغة كوتلن. لمُتابعة الدروس منذ البداية ومُشاهدة فهرس المحتويات يمكنك الانتقال إلى الدرس الأول من هنا.

هل أعجبك المحتوى وتريد المزيد منه يصل إلى صندوق بريدك الإلكتروني بشكلٍ دوري؟
انضم إلى قائمة من يقدّرون محتوى إكسڤار واشترك بنشرتنا البريدية.