تعلّم البرمجة بلغة كوتلن (50): التعامل مع القوائم Lists

تعلّم البرمجة بلغة كوتلن (50): التعامل مع القوائم Lists
أستمع الى المقال

درسنا حتى الآن في دورة تعلّم البرمجة بلغة كوتلن، ثلاثة أنواع من التجميعات، القوائم Lists والأطقم Sets والخرائط Maps. ومن بين هؤلاء الثلاثة، تعتبر القوائم Lists هي الأشهر والأكثر استخدامًا، في البرامج المكتوبة بلغة كوتلن. لذا، تعلّمنا في درس العمليات على التجميعات، بعض طرق التعامل مع القوائم، عبر استخدام الدوال من مكتبة كوتلن القياسية.

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

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

الربط بين قائمتين Zipping:

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

لدينا في الشفرة أعلاه، قائمة numbers تحتوي على عناصر من الأعداد الصحيحة Int. والقائمة الثانية numbersText، تحتوي على عناصر String، وهي تُمثل الأعداد كتابةً.

ستبدأ دالة ()zip عملية الربط بين القائمتين، بأول عنصر في القائمة الأولى التي تستدعيها، ثم يكون العنصر الأول من القائمة الثانية، هو التالي في عملية الربط، بنفس طريقة عمل السوستة.

ولأننا استدعينا دالة ()zip مع القائمة numbers، ستأخذ الدالة العنصر الأول من هذه القائمة وهو العدد 0، ثم العنصر الأول من القائمة numbersText وهو النص “Zero”، وتضعهما معًا في كائن Pair. (شرحنا هذه الكائن في هذه الفقرة من درس البرمجة المُعَمَمّة).

وسيكون شكل العنصرين معًا:

Pair<Int, String>(0, "Zero")

وهكذا ستتعامل الدالة ()zip مع باقي العناصر في القائمتين:

Pair<Int, String>(1, "One")
Pair<Int, String>(2, "Two")
Pair<Int, String>(3, "Three")
Pair<Int, String>(4, "Four")

ثم تضع الدالة كل هذه العناصر في قائمة جديدة، وهي التي سيتم إسنادها إلى المتغير pairList. لذلك، سيكون نوع بيانات المتغير pairList هو:

List<Pair<Int, String>>

أي قائمة List تحتوي على عناصر من Pair، والذي بدوره يحتوي على عنصرين من النوعين Int و String على التوالي.

وعند تنفيذ الشفرة، ستكون نتيجة طباعة قيمة المتغير pairList، هي:

ربط القائمة مع النطاقات Ranges:

الربط بنطاق عددي IntRange:

يمكن استخدام دالة ()zip لربط القوائم بنطاق عددي. فإذا استبدلنا قائمة numbers بالنطاق 4..0:

numbersText.zip(0..4)

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

وبالطبع، يمكننا جعل النطاق العددي هو الذي يستدعي دالة ()zip، بالتالي سيكون العنصر الأول في كائن Pair في القائمة الجديدة هو عنصر هذا النطاق:

(0..100).zip(numbersText)

ولمعرفة كيف تعاملت الدالة مع الطريقتين، يمكننا وضعها في دالة ()main وتنفيذ الشفرة:

نلاحظ أنه عند استدعاء الدالة مع النطاق 100..0، أخذت الدالة أول خمسة أعداد فقط من النطاق وربطتهم بقائمة numbersText، وتجاهلت باقي عناصر النطاق. هذا لأن القائمة numbersText تحتوي على خمسة عناصر فقط. 

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

الربط بنطاق حرفي CharRange:

بنفس طريقة النطاق العددي، يمكن استخدام الدالة مع نطاق من الحروف:

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

إجراء عمليات قبل عملية الربط:

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

val titles = listOf(
    "Lists",
    "Data Classes",
    "Lambdas"
    )
val ids = listOf(30, 38, 46)

يمكننا الربط بين القائمتين كما فعلنا في الفقرة السابقة، باستدعاء دالة ()zip عبر قائمة titles، كالتالي:

titles.zip(ids)

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

[(Lists, 30), (Data Classes, 38), (Lambdas, 46)]

أو استدعاء الدالة عبر قائمة ids:

ids.zip(titles)

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

[(30, Lists), (38, Data Classes), (46, Lambdas)]

في الحالتين، تم إنتاج قائمة تحتوي على كائنات Pair من عناصر القائمتين. ولكن، كما نرى في نتيجة الطباعة، ليس من المفهوم ما الذي تُمثله هذه القائمة، فهي نص يتبعه رقم أو العكس.

لجعل هذه القوائم مفهومة أكثر، يمكننا الاستفادة من معرفتنا السابقة بأصناف البيانات. لأن هذه الدروس هي عبارة عن كائنات. لذا، من الأفضل أن نوفر لها صنف بيانات يُمثلها ونضع به خاصيّاتها.

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

استخدام صنف بيانات لتمثيل كائنات الدروس:

data class Tutorial(
    val id: Int,
    val title: String
)

الصنف Tutorial سيمكننا من إنشاء كائن درس، في كل مرة يتم استدعاء الباني الخاص به، وإرسال الخاصيّتين الخاصتين بالدرس المُراد: الاسم name و رقم الدرس id.

ما نريده من دالة ()zip هذه المرة، هو بعد أن تنشئ لنا القائمة التي عناصرها كائنات الـ Pair، أن تحوّل هذه العناصر، إلى النوع الخاص بنا Tutorial. تمتلك الدالة، معامل من نوع بيانات الدالة، لذلك يمكننا أن نرسل لها تعبير لامبدا به شروط أو قاعدة التي ستعتمد عليها لإجراء هذا التحويل.

ولأن هذا المعامل هو الأخير في دالة ()zip، يمكننا وضع تعبير اللامبدا خارج أقواس الدالة، كما شرحنا في درس اللامبدا:

val tutorials = titles.zip(ids) { name, id ->
        Tutorial(id, name)
}

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

titles.zip(ids)

سينتج الشفرة التالية:

[(Lists, 30), (Data Classes, 38), (Lambdas, 46)]

ثم وقبل إسناد هذه النتيجة إلى المتغير tutorials، سيتم تمرير هذه العناصر إلى تعبير اللامبدا بواقع عنصر في كل مرة. ولأن كل عنصر يتكون من كائن نصي String وكائن عددي Int، سنستقبل الكائن النصي في معامل name والكائن العددي في معامل id في تعبير اللامبدا (يمكننا تسمية المعاملين كما نريد). ثم ننشئ كائن Tutorial عبر تمرير هذين المعاملين إلى خاصيّاته.

لتكون الشفرة الكاملة كالتالي:

وعند تنفيذ هذه الشفرة، ستكون نتيجة الطباعة:

كما يظهر في الصورة، تحتوي قائمة tutorials كائنات درس Tutorial، وليس مجرد أزواج من الكائنات.

دالة ()zipWithNext:

يشبه عمل هذه الدالة عمل دالة ()zip كثيرًا. الاختلاف بين الدالتين، يكمن في أن دالة ()zipWithNext، ستقوم بإنشاء كائن Pair من نفس عناصر القائمة. أي أننا سنستدعيها عبر قائمة واحدة فقط وليس قائمتين:

عند استدعاء دالة ()zipWithNext عبر قائمة الحروف letters، ستكون النتيجة قائمة جديدة عناصرها هي من كائنات Pair. سيتم وضع الحرف a والحرف الذي يليه في القائمة b، في كائن Pair واحد، وسيُمثلان معًا العنصر الأول في القائمة الجديدة. 

أمّا العنصر الثاني في القائمة، فسيكون كائن Pair من الحرف b والحرف الذي يليه c. ثم العنصر الأخير، سيكون الحرف c مع الحرف d. ليكون شكل القائمة الجديدة، كالتالي:

[(a, b), (b, c), (c, d)]

وعند استدعاء نفس الدالة مع قائمة letters، ثم تمرير تعبير لامبدا، سيتم تنفيذ التعبير ثم إعادة القائمة حسب نتيجة شرط تعبير اللامبدا، وإسنادها إلى المتغير newList. ليكون شكل القائمة هذه المرة، كالتالي:

[ab, bc, cd]

وهو ما تظهره الصورة التالية لتبويب Run في برنامج IntelliJ IDEA:

تسطيح القوائم Flattening:

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

فمثلًا، إذا كان لدينا القائمة التالية:

val list: List<List<Int>> = listOf(
        listOf(1, 2, 3, 4),
        listOf(5, 6),
        listOf(7, 8, 9),
    )

ستكون نتيجة الطباعة لهذه القائمة هي:

[[1, 2, 3, 4], [5, 6], [7, 8, 9]]

فكما نرى في النتيجة، كل عناصر المتغير list، عبارة عن قوائم List تحتوي على عناصر من النوع Int. لذلك نوع بيانات المتغير list، هو:

List<List<Int>>

لدمج كل عناصر القوائم هذه في قائمة واحدة، يمكننا استخدام دالة ()flatten:

val newList: List<Int> = list.flatten()

عند استدعاء الدالة مع القائمة list، ستعيد قائمة جديدة من كل عناصر القوائم داخلها، وهي التي سيتم إسنادها للمتغير newList. وستكون نتيجة الطباعة للقائمة الجديدة هي:

[1, 2, 3, 4, 5, 6, 7, 8, 9]

تم التخلص من كل القوائم والإبقاء فقط على عناصرها. ولذلك يكون نوع بيانات المتغير newList هو:

List<Int>

لتكون الشفرة الكاملة كالتالي:

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

دالتي ()map و ()flatten:

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

كمثال، إذا كان لدينا القائمة التالية:

val rgb: List<String> = listOf(
    "Red",
    "Green",
    "Blue"
)

عند طباعة قيمة المتغير rgb، ستكون النتيجة:

[Red, Green, Blue]

وهي قائمة عناصرها من النوع String، لذلك نوع المتغير rgb هو:

List<String>

فإذا أردنا تحويل عناصر القائمة من مجرد كونها كائن نصي String، إلى قائمة من النوع Char. أي أن كل حرف في هذه القائمة، سيكون عنصرًا قائمًا بنفسه ولا يتبع أي نص. للقيام بهذا الأمر، سنستخدم دالة ()map أولًا.

تحويل عناصر القائمة عبر ()map:

val rgbTransform: List<List<Char>> = rgb.map { 
    it.lowercase().toList() 
}

الشرط الذي وضعناه في تعبير اللامبدا المُرسل لدالة ()map، هو خذ كل عنصر من قائمة rgb وغيّره إلى حروف صغيرة عبر دالة ()lowercase، ثم حوله إلى قائمة List عبر دالة ()toList. وعند انتهاء التحويل، سيتم إسناد القائمة الجديدة إلى المتغير rgbTransform. ونتيجة طباعة قيمة هذا المتغير ستكون:

[[r, e, d], [g, r, e, e, n], [b, l, u, e]]

كما نرى، تم تحويل العنصر Red مثلًا، إلى [r, e, d]. حيث تم تحويل حرف الـ R إلى الحرف الصغير، ثم تم وضع كل حروفه، كعناصر قائمة منفصلة داخل القائمة الجديدة. لذلك نوع بيانات المتغير rgbTransform هو:

List<List<Char>>

وبما أن ()map أنتجت لنا قائمة ثنائية الأبعاد، أي قائمة عناصرها نفسها، هي عبارة عن قوائم، يمكننا استخدام دالة ()flatten لأخذ عناصر كل القوائم، ووضعها في قائمة جديدة.

تسطيح القائمة عبر ()flatten:

val rgbFlatten: List<Char> = rgbTransform.flatten()

ستعمل ()flatten، على أخذ العناصر فقط من كل القوائم، ووضعها في قائمة جديدة، ثم يتم إسناد القائمة الجديدة للمتغير rgbFlatten:

[r, e, d, g, r, e, e, n, b, l, u, e]

ولأن عناصر القائمة الجديدة عبارة عن حروف فقط، يكون نوع بيانات المتغير rgbFlatten هو:

List<Char>

وعند وضع كل هذه الأسطر في دالة ()main، سيكون شكل الشفرة هو:

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

تم إتمام المهمة عبر دالتي ()map و ()flatten اللتين حولتا القائمة ذات العناصر النصية، إلى قائمة ذات عناصر حرفية. ولكن، يتوفر في مكتبة كوتلن دالة واحدة تمزج بين عمل الدالتين، وهي دالة ()flatMap.

دالة ()flatMap:

عمل هذه الدالة، هو مزيج من عمل دالة ()map ودالة ()flatten. فإذا أعدنا كتابة الشفرة في الفقرة السابقة، واستخدمنا هذه الدالة فقط، لتحقيق نفس المهمة، سيكون شكل الشفرة هو:

وستكون نتيجة طباعة قيمة المتغير flatMap هي:

[r, e, d, g, r, e, e, n, b, l, u, e]

لذلك نوع بياناته هو:

List<Char>

وبالطبع، يمكننا إختصار الشفرة أكثر، وحذف المتغير و استدعاء الدالة مباشرةً على القائمة:

وستكون نتيجة طباعة هذه الشفرة عند تنفيذها هي:

الخلاصة:

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

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

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