تعلّم البرمجة بلغة كوتلن (48): مراجع أعضاء الصنف Member References

تعلّم البرمجة بلغة كوتلن (48): مراجع أعضاء الصنف Member References
أستمع الى المقال

تعلمنا في الدرسين السابقين، كيفية إرسال تعابير دوال اللامبدا، كقيمة إلى الدوال التي لديها معامل من نوع بيانات الدوال. ولكن ماذا لو كنا نريد إرسال دالة عادية وليس لامبدا، كقيم لمعاملات هذه الدوال؟

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

بالإضافة إلى دوال الصنف، يمكننا استخدام هذا العامل مع: الخاصيّات Properties وحتى بواني الصنف Constructors، لينتج لنا ما يعرف بـ تعبير مرجع العضو Member Reference Expression. هذا التعبير، سيكون هو المرجع Reference الذي يُمثِّل هذا العضو.

تعبير مرجع الخاصيّة:

كمثال، إذا كان لدينا صنف بيانات لتمثيل كائن (رسالة) اسمه Message. ولدى هذا الصنف ثلاث خاصيّات: اسم مُرسل الرسالة sender من النوع String، ونص الرسالة text من النوع String أيضًا، وخاصية منطقية Boolean تحدد ما إذا تم قراءة الرسالة أم لا:

data class Message(
    val sender: String,
    val text: String,
    val isRead: Boolean
)

فإذا افترضنا أنه لدينا قائمة List عناصرها كائنات من نوع الصنف Message أعلاه:

val messages = listOf(
  Message("Mama", "Hey! I miss you", true),
  Message("Brother", "Where are you?", false),
  Message("Boss", "Meeting today", false)
)

أنشأنا القائمة messages بثلاث كائنات Message، الأولى من مرسلة من Mama وبها نص text وخاصيّة isRead بها تحمل القيمة true، أي تمت قراءتها. أمّا الرسالتان الأخريتان، بجانب اسم المرسل والنص، تحمل خاصيّة isRead الخاصة بهما، القيمة false، أي لم تتم قراءتهما.

فإذا أردنا تصفية الرسائل ومعرفة ما هي الرسائل التي لم تتم قراءتها بعد، يمكننا استخدام دالة ()filterNot من مكتبة كوتلن القياسية، وهي عكس دالة ()filter التي استخدمناها في الدرس السابق

وتمامًا مثل دالة ()filter، لدى دالة ()filterNot، معامل من نوع بيانات الدوال. أي أننا يمكن أن نرسل لها تعبير لامبدا، أو تعبير مرجع العضو، والذي سيكون في مثالنا هذا هو الخاصيّة isRead:

val unread = messages.filterNot(
    Message::isRead 
)

نحن هنا نستدعي الدالة ()filterNot عبر القائمة messages، ونرسل لها القاعدة التي على أساسها ستصفي عناصر القائمة messages، وتعيد لنا الرسائل التي لم يتم قراءتها. هذه القاعدة هي تعبير مرجع عضو الصنف Message الخاصيّة isRead.

ستنشئ دالة ()filterNot، قائمة جديدة تحتوي على العناصر التي تكون بها الخاصيّة isRead تساوي false. هذه القائمة، سيتم إسنادها للمتغير unread. لذا، عند طباعة هذا المتغير، ستكون نتيجة الطباعة هي قائمة تحتوي على كائنات الرسائل التي لم يتم قراءتها:

استخدام تعابير اللامبدا:

وبالطبع، كان يمكننا بدل إرسال الخاصيّة isRead بهذه الطريقة، أن نرسلها كتعبير لامبدا:

val unread = messages.filterNot { msg -> 
    msg.isRead 
}

حيث سيُمثل المعامل الذي أسميناه msg، عنصر واحد من القائمة في كل مرة، حتى آخر عنصر فيها. ولأن العنصر msg عبارة عن كائن من Message، تتوفر له الخاصيّة isRead والتي وصلنا إليها باستخدام النقطة ( . ) بعد اسم الكائن.

وكما ذكرنا في الدرس الأول من هذا القسم، يمكننا استبدال المعامل msg بالكلمة it:

val unread = messages.filterNot { 
    it.isRead
}

في هذه الحالة أيضًا، ستكون نتيجة الطباعة هي نفسها كالسابق. 

كما نرى، لدينا طريقتان مختلفتان استخدمناهما لنفس الغرض، وهو تصفية الرسائل التي لم يتم قراءتها. إذًا، ما هو الفرق بين الطريقتين؟ هذا ما سنعرفه في الفقرة التالية.

الفرق بين تعبير اللامبدا وتعبير مرجع العضو:

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

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

لفعل ذلك، نحتاج دالتين من مكتبة كوتلن القياسية. الدالة الأولى هي ()sortedWith والتي لديها معامل من النوع Comparator، وهو صنف يتوفر في كوتلن لإجراء مقارنات بين الكائنات من أي نوع بيانات.

والدالة الأخرى، هي ()compareBy. وهي ستعيد لنا كائن Comparator، بعد تطبيق سلسلة من الدوال على القيم التي تُمرر لمعاملها، لحساب نتيجة المقارنة. ومعامل هذه الدالة هو من نوع بيانات الدوال ويحمل الكلمة المفتاحية vararg، لذا يمكنها أن تستقبل أي عدد من الكائنات كقيم لهذا المعامل.

ولأن دالة ()compareBy تنتج لنا كائن مقارنة Comparator، سنمررها مباشرةً، لدالة ()sortedWith، والتي بدورها ستعيد لنا قائمة مرتبة، حسب الشروط التي أرسلناها لدالة ()compareBy:

val sortedMessages = messages.sortedWith( 
    compareBy(
        Message::isRead,
        Message::sender 
    )
)

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

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

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

تحسين الطباعة باستخدام دوال البرمجة الوظيفية:

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

val sortedMessages =
    messages.sortedWith( 
        compareBy(
            Message::isRead,
            Message::sender 
        )
    ).joinToString(",") { "$it\n" }
.replace(",", "")

       

للقيام بذلك، قمنا أولًا باستخدام دالة ()joinToString على القائمة messages، مباشرةً بعد ترتيبها عبر دالة ()sortedWith. ستعمل دالة ()joinToString على تحويل القائمة إلى نص عادي مكوّن من عناصر القائمة، وهي في قائمة messages عبارة عن كائنات من النوع Message.

ستتعرّف الدالة على الفاصل بين هذه العناصر، باستخدام القيمة الأولى التي أرسلناها إليها، وهي رمز الفاصلة ( , ) التي تفصل بين العناصر. أمّا معاملها الثاني، فهو من نوع بيانات الدوال، لذا أرسلنا إليه تعبير لامبدا، لتقوم على أساسه بوضع سطر جديد بعد كل عنصر في القائمة، باستخدام الكلمة it التي تُمثِّل العنصر، ورمز السطر الجديد ( n\ ).

الآن بعد أن تم تحويل عناصر قائمة messages، إلى نص String عادي من ثلاثة أسطر حسب عدد عناصر القائمة، يمكننا استخدام دالة ()replace المتوفرة للنوع String. مهمة هذه الدالة، هي استبدال الفاصلة ( , ) الموجودة في النص، بنص فارغ “”.

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

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

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

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

تعبير مرجع الدالة العضو في صنف:

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

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

fun Message.isImportant(): Boolean =
    this.text.contains("Salary") ||
            this.attachment?.any { 
                it.type == "image" || 
                it.type == "pdf"
            } == true

ألحقنا دالة ()isImportant، بالصنف Message. والشفرة يمين علامة الإسناد ( = )، هي من النوع المنطقي الذي يعيد نتيجة true أو false. هذه النتيجة هي ما ستعيده الدالة، وهو ما شرحناه تفصيليًا في درسي الدوال والقيم المنطقية

عند استدعاء هذه الدالة مع كائن من النوع Message، ستعيد الدالة القيمة true، إذا كان هذا الكائن يحتوي على الكلمة Salary، أو به مرفقات من النوع image أو pdf. بالنسبة لخاصيّة attachment، فهي من النوع قائمة List تحتوي على عناصر من الصنف Attachment:

data class Attachment(val type: String)

والخاصيّة attachment من النوع الذي يقبل القيم الفارغة Nullable، لأنه قد لا يتم إرفاق مرفقات مع الرسالة. أضفنا هذه الخاصيّة للصنف Message كالتالي:

data class Message(
    val sender: String,
    val text: String,
    val isRead: Boolean,
    val attachment: List<Attachment>?
)

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

الحل بكل بساطة، نرسل هذه الدالة كتعبير مرجع باستخدام العامل ( :: )، تمامًا مثلما فعلنا مع الخاصيّات:

val important = messages.filter(
    Message::isImportant
)

هذا التعبير:

Message::isImportant

سيجعل دالة ()filter تصفي قائمة الرسائل messages، وتضيف العناصر التي توافق المعايير التي تضعها دالة ()isImportant. 

وبعد إجراء هذه التعديلات على الشفرة، أصبح شكلها كالتالي:

نلاحظ أننا استخدمنا أيضًا، دالة ()forEachIndexed. وهي دالة ستدور على عناصر القائمة عنصرًا بعد عنصر، مثل دالة ()forEach. والفرق بينهما، هو أن الدالة الأولى، ستمكننا من الوصول لفهرس index القائمة بجانب عناصرها. ولأنه أصبح لدينا معاملين في تعبير اللامبدا الخاص بـ ()forEachIndexed، لن نستطيع استخدام الكلمة it، ويجب وضع متغيرات تُمثل المعاملين صراحةً، ثم السهم <-.

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

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

تعبير مرجع دوال ذات مستوى أعلى Top-Level Functions:

بخلاف لغة الجافا، والتي يجب أن توضع الدوال فيها داخل صنف class أو واجهة interface، يمكن كتابتها في كوتلن داخل حزمة package أو ملف file، دون الحاجة إلى إنشاء صنف خاص بها. ويمكن استدعاء هذه الدوال، باستخدام اسمها مباشرةً، دون الحاجة إلى إنشاء كائن من صنف ما، كما نفعل مع الدوال العضوة المنتمية لصنف معين Member Functions، أو المُلحقة به Extension Functions. ومن أهم هذه الدوال ذات المستوى الأعلى، لدينا دالة ()main.

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

fun ignore(message: Message) =
 !message.isImportant() &&
    message.sender !in setOf("Boss", "Mom")

لدى دالة ()ignore معامل واحد من النوع Message. والشفرة المنطقية يمين علامة الإسناد ( = )، ستعمل على تجاهل الرسائل، حسب معيارين يجب أن يتحققا في الوقت نفسه. أحدهما هو ألّا تكون الرسالة مهمة important وهو ما ستحدده دالة ()isImportant. والمعيار الثاني، استخدمنا الكلمة in، للتاكد من ألّا يكون اسم مرسل الرسالة، من بين الأسماء في قائمة Set التي بها عنصرين.

وهذا يعني، عند تصفية الرسائل باستخدام ()filterNot مثلًا، سيتم تجاهل وإقصاء الرسائل التي ينطبق عليها المعياران في الدالة، والإبقاء على غيرها. ولكن لفعل ذلك، يجب أولًا أن نرسل هذه الدالة إلى دالة ()filterNot:

val notIgnoredMessages = 
    messages.filterNot(::ignore)

كما نرى في السطر أعلاه، استخدمنا العامل ( :: ) مباشرةً مع دالة ()ignore. وهذا لأنها دالة ذات مستوى أعلى تعيش داخل الملف ولا تنتمي لأي صنف. 

وعند تنفيذ الشفرة بعد إضافة دالة ()ignore، ستكون نتيجة الطباعة:

تعبير مرجع باني الصنف Constructor:

يمكننا إنشاء مرجع إلى باني الصنف، باستخدام العامل ( :: ) واسم الباني. ولفهم كيفية استخدامه، دعونا ننظر إلى البرنامج التالي:

لدينا قائمة names بها أسماء عدة طلاب كعناصر. ولدينا صنف بيانات لتمثيل الطلاب به خاصيّتان. الأولى الرقم التعريفي للطالب id، والثانية اسم الطالب name. ما نريده، هو إنشاء كائنات Student، باستخدام أسماء الطلاب من القائمة names، وأن يكون الرقم التعريفي تلقائي حسب فهرس الاسم في القائمة.

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

الشفرة أعلاه، ستقوم بالمهمة المطلوبة. ولكن، يمكننا فعل نفس الأمر، بإرسال تعبير مرجع الباني إلى دالة ()mapIndexed، بدلًا عن تعبير اللامبدا:

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

الخلاصة:

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

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

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

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