تعلّم البرمجة بلغة كوتلن (34): وظائف إضافية Extension Functions

تعلّم البرمجة بلغة كوتلن (34): وظائف إضافية Extension Functions
أستمع الى المقال

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

لا تختلف لغات برمجة الكمبيوتر كثيرًا في ما تجعله ممكنًا، ولكن فيما تجعله سهلاً.

لاري وول, مخترع لغة بيرل Perl

في هذا القسم، ندرس المزيد من المفاهيم التي طبقتها كوتلن في سهولة أو قابلية الإستخدام Usability، بجعل الممارسة الجيدة في كتابة الشفرات أسهل، والممارسة السيئة أصعب. تعلّم هذه المفاهيم، لا يجعل من المبرمجين أكثر كفاءة وسريعي الإنتاج للبرامج فحسب، بل يجعلهم ينتجونها بأقل قدر ممكن من الأخطاء. ونبدأ بأول درس، الوظائف الإضافية Extension Functions.

ماهي الوظائف الإضافية:

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

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

إنشاء الدوال الملحقة:

للإعلان عن دالة ملحقة، نبدأ بإسم الصنف أو نوع البيانات المستقبل لهذه الدالة والمراد تمديد وظائفه receiver type، ثم نقطة (.)، ثم اسم الدالة:

fun ReceiverType.extensionFunction() { … }

بدأنا بكلمة fun، ﻷنها دالة عادية، مع إختلاف أنها تبدأ باسم الصنف أو نوع البيانات ReceiverType. لأننا نريد منها أن تضيف وظيفة لهذا الصنف أو نوع البيانات.

طباعة الحرف الأول والأخير في نص String:

إذا افترضنا أنه لدينا نص معين، ونريد أن نطبع الحرف الأول والأخير في هذا النص، يمكننا فعل ذلك باستخدام دالتي ()first و ()last من مكتبة كوتلن القياسية:

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

FirstLastCharString

طباعة الحرف الأوسط في النص String:

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

كما نرى في الصورة، أنه لاتوجد دالة بهذا الاسم في الصنف String، لذا تظهر باللون الأحمر في برنامج IntelliJ. وحتى عند تجاهل الخطأ وتنفيذ البرنامج، لن يتم ترجمة الشفرة، وسينتج الخطأ:

Unresolved reference: middle

وهذا ﻷن مترجم كوتلن لم يجد دالة باسم ()middle، مرتبطة بالصنف String. إذًا، ما هو الحل؟

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

إضافة وظيفة للنوع String:

لأن نوع البيانات الأساسي String هو من الأصناف التي تأتي جاهزة مع لغة كوتلن ولا يمكننا التعديل عليها، يمكننا إضافة وظيفة إليه فقط عبر ميزة Extension Function التي توفرها كوتلن:

أنشأنا دالة باستخدام الكلمة المفتاحية fun، وأسميناها middle، وجعلنا الدالة تعيد النوع Char. ولأن النوع String هو عبارة عن تجميعة من المحارف Char، يمكننا التعامل معه مثل قائمة List غير قابلة للتغيير. بالتالي، للوصول إلى أي عنصر (محرف) في النص String، يمكننا استخدام الفهرس الذي يُمثله.

لذلك، داخل الأقواس المعقوفة للدالة، أعلنا عن متغير middleCharIndex من النوع Int، ليُمثل فهرس الحرف الأوسط في النص String. والتعبير الذي أسندناه للمتغير middleCharIndex، هو عدد محارف النص، الذي استخدمنا الكلمة المفتاحية this والخاصية length من الصنف String لإيجاده، ثم قسمنا الناتج على الرقم 2.

فمثلًا، إذا كان عدد محارف النص 5 محارف، 5 / 2 = 2، بالتالي ستكون قيمة المتغير middleCharIndex تساوي 2. وكنتيجة، ستعيد الدالة العنصر الذي فهرسه 2:

return this[2]

بالطبع 5 تقسيم 2 يساوي 2.5، ولكن كما قلنا في درس أنواع العدد Number Types، أنه عند تقسيم عدد صحيح Int على عدد صحيح آخر، يتم تجاهل الفاصلة العشرية وما بعدها، لأن هذه هي الطريقة القياسية عند التعامل مع الأعداد الصحيحة من نوع البيانات Int.

ولأنه لايوجد فهرس بكسور عشرية، كان استخدامنا لمتغير من النوع Int مناسب جداً في حالتنا هذه، لأنه سيتم التخلي عن الفاصلة العشرية وما بعدها من أرقام عند التقسيم. ثم جعلنا الدالة تعيد الحرف الذي يُمثله رقم الفهرس الناتج من التقسيم، باستخدام الكلمة this مرة أخرى. إذًا، ماذا تعني الكلمة المفتاحية this؟

الكلمة المفتاحية this:

هي كلمة تشير إلى الكائن الذي تم استدعاء الدالة الملحقة بواسطته باستخدام النقطة (.). فمثلًا، في مثالنا أعلاه، تشير الكلمة this إلي الكائن النصي “ExVar”، والذي يُمثله المتغير platform. فعند كتابة:

platform.middle()

سيتم تعويض الكائن platform في الدالة ()middle. وهذا يعني كأننا كتبنا:

بالطبع هذه الشفرة للشرح فقط ولا يمكننا كتابة المتغير platform مباشرة في دالة ()middle، لأنه خاص بالدالة ()main فقط، ولا تعرف أية دالة غيرها بوجوده. لذا نستعيض عن المتغير platform بالكلمة المفتاحية this، في دالة ()middle المضافة للصنف String.

تعديل الشفرة لطباعة الحرف الأوسط:

يمكننا تعديل الشفرة وإيجاد الحرف الأوسط للكلمة “ExVar” باستخدام دالتنا المضافة للصنف String:

وعند تنفيذ الشفرة مرة أخرى، ستكون النتيجة:

FirstMiddleLastCharString

اختصار الدالة ()middle:

ويفضل دائماً اختصار الشفرات في كوتلن، إذا كانت ستزيد من قابلية القراءة. وفي دالة ()middle، يمكننا فعل التالي:

fun String.middle() = this[this.length / 2]

ما زالت الدالة مقروءة، لأن التعبير this.length / 2 ينتج رقم معين، وهو الذي سيتم استخدامه كفهرس لإيجاد المحرف في السلسلة النصية التي يُمثلها الكائن this. تخلينا أيضًا عن نوع الإرجاع Char للدالة، لأن كوتلن ستتعرف عليه من التعبير يمين علامة الإسناد =.

الفرق بين الدالة العضو member function والدالة الملحقة Extension function:

دعونا نفترض أنه لدينا الصنف التالي:

الصنف A يحتوي على دالة عضو اسمها member. ثم لاحقًا كتبنا له دالة ملحقة اسمها extension. كلتا الدالتين يمكن استدعائهما بنفس الطريقة:

val a = A()

a.member()

a.extension()

أو

A().member()

A().extension()

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

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

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

برنامج طباعة الأرقام المزدوجة:

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

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

  1. في بداية البرنامج نطلب من مستخدم البرنامج إدخال الأعداد الصحيحة مفصولة بمسافات. ثم نقرأ المدخلات عبر دالة ()readln، ونستخدم دالة ()split ونرسل لها مسافة ” ” كمعامل، لتستخدمها في فصل المدخلات عن بعضها، وتعيد لنا قائمة List بها مدخلات المستخدم كعناصر. سيتم تخزين القائمة في المتغير input.
  2. ننشئ قائمة من النوع Int قابلة للتغيير باستخدام دالة ()<mutableListOf<Int، ونسندها للمتغير numbers. نحتاج هذه القائمة، ﻷن قائمة المدخلات input، هي عبارة عن عناصر من النوع String، ونحن نريد قائمة من النوع Int. لذا سندور على عناصر input عبر استخدام حلقة for، ونحولها إلى النوع Int باستخدام دالة ()toInt، ونضيفها إلى قائمة numbers عنصرًا بعد آخر. ولأن هذه العملية قد تنتج خطأ إستثناء إذا لم تستطع دالة ()toInt تحويل العنصر إلى Int، وضعناها في كتلة try-catch. (بالطبع توجد في كوتلن طريقة أفضل وأقصر من هذه لفعل نفس الأمر باستخدام { } map. ولكن في هذا الوقت المبكر من الدورة، لن يكون من الملائم عرضها، لأنها معلومات لم ندرسها بعد).
  3. في كتلة if، نتأكد من أن قائمة numbers ليست فارغة، باستخدام دالة ()isNotEmpty. فإذا لم تكن القائمة فارغة، نرسل إلى دالة الطباعة ()println، النتيجة العائدة من الدالة الملحقة ()onlyEvenNumbers.
  4. ما نريده من البرنامج، هو فقط طباعة الأعداد المزدوجة من المدخلات. ولأن الصنف <MutableList<Int لا يملك دالة توفر هذه الوظيفة، ولا نستطيع تعديل الصنف في مكتبة كوتلن القياسية، أنشأنا له دالة ملحقة تقوم بهذه المهمة. أعطينا الدالة اسم مناسب لعملها onlyEvenNumbers. وجعلناها تعيد قائمة من النوع <MutableList<Int أيضًا، لأن الأعداد المزدوجة قد تكون أكثر من عدد واحد. ثم أنشأنا قائمة مؤقتة evenNumList، وهي التي سنعيدها بعد تعبئتها بالأعداد المزدوجة. أما في حلقة for، سندور على عناصر القائمة this. و this هنا تعني القائمة numbers، لأنها هي التي تستدعي دالة ()onlyEvenNumbers في هذه اللحظة. ثم نتأكد في كتلة if، من أن العنصر الحالي في this، هو عدد مزدوج أم لا، باستخدام طريقة القسمة بباقي. فإذا كان العدد بعد القسمة على الرقم 2 تبقى منه صفر، يتم إضافته لقائمة evenNumList، أما غير ذلك، لا تفعل شيئًا. وبالطبع ستواصل حلقة for عملها حتى نهاية العناصر في this. وفي النهاية ستعيد الدالة القائمة evenNumList. وهو ما سيتم إرساله لدالة الطباعة ()println.

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

كما نرى في الصورة، تم تجاهل الأعداد الفردية وطباعة فقط الأعداد المزدوجة.

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

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

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

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