تعلّم البرمجة بلغة كوتلن (44): خاصيّات إضافية Extension Properties

تعلّم البرمجة بلغة كوتلن (44): خاصيّات إضافية Extension Properties
أستمع الى المقال

شرحنا في درس التغليف Encapsulation، كيف يمكننا حماية الخاصيّات Properties، باستخدام محددات الوصول Access Modifiers، و دالتي ()set و ()get. واستعرضنا في درس الوظائف الإضافية Extension Functions، كيف يمكننا تمديد وظائف الأنواع والأصناف، بإلحاق دوال بها. 

في هذا الدرس، سنتعرّف على كيفية أنشاء خاصيّات إضافية للأصناف، بنفس طريقة الدوال الملحقة. وأيضًا، سنتعلم المزيد عن استخدام الـ setters والـ getters، وكيفية الإستغناء عن الدوال Functions إذا كانت العملية المُراد إجراؤها بسيطة، واستخدام الخاصيّات Properties بدلًا عنها.

الوصول للخاصيّة عبر ()set و ()get:

كما نعلم، أن الأصناف في كوتلن، يمكن ألّا تحتوي على أية خاصيّة، أو أن تحتوي على خاصيّة واحدة أو أكثر. ولكن عمليًا، يتم وضع الخاصيّات في أغلب الأصناف التي يحتويها مشروع كوتلن.

هذه الخاصيّات، يجب حمايتها بتغليفها، عبر محددات الوصول. حينها يمكننا التعامل معها، بتغيير قيمها عبر ()set، أو الحصول على قيمها عبر ()get. في الفقرات أدناه، سنلقي نظرة فاحصة على كيفية القيام بذلك.

استخدام ()get:

إذا كان لدينا صنف به خاصيّة واحدة:

class Student {

    var name = “Unknown”

}

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

fun main() {

    val student = Student()

    println(student.name)

}

سيتم طباعة النص Unknown وهو قيمة name. يبدو الأمر سهلًا وبسيطًا. ولكن في واقع الأمر، تُنشئ كوتلن دالة خاصة اسمها ()get وتعرف بالـ getter، تلقائيًا للخاصيّة name. ما يحدث في الخلفية عند الإعلان عن أية خاصيّة في الصنف يشبه التالي:

لابد من كتابة ()get تحت الخاصيّة مباشرةً. الكلمة field داخل الدالة، المقصود منها الخاصيّة أعلاها، وهكذا سيفهمها المترجم. لذا نستخدم الكلمة return، والتي ستعيد قيمة الخاصيّة.

وكما عرفنا من درس الدوال، يمكن التخلص من الأقواس المعقوفة والكلمة return، واستبدالهما بعلامة الإسناد ( = ):

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

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

تخصيص ()get:

يمكننا تخصيص ()get، ووضع الشفرة التي نريدها بشرط أن يكون السطر الأخير به الكلمة return لإعادة قيمة الخاصية. لأن عمل الدالة أساسًا، هو إعادة قيمة الخاصيّة. 

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

والآن بمجرد استدعاء الخاصيّة name، سيتم تنفيذ أمر الطباعة أولًا، ثم إعادة قيمة الخاصيّة:

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

استخدام ()set:

عند استدعاء خاصيّة ما، يمكن أن نغير قيمتها إذا تم الإعلان عنها باستخدام الكلمة var. ولأننا أعلنّا عن name باستخدام var، يمكننا فعل التالي:

عند تنفيذ هذه الشفرة، سيتم تغيير قيمة الخاصيّة name من Unknown إلى Mohammad. لأن الخاصيّة من النوع var، ستوفر لها كوتلن في الخلفية، دالة ()set والتي تعرف بالـ setter. فنحن حينما نستخدم علامة الإسناد ( = ) مع الخاصيّة، نكون تلقائيًا قد استدعينا هذه الدالة. وتكون في أبسط صورها:

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

تخصيص ()set:

تمامًا مثلما فعلنا مع دالة ()get، يمكننا كتابة الشفرة التي نريدها داخل ()set:

والآن حينما نحاول تغيير قيمة الخاصيّة، يتم استدعاء دالة ()set التي خصّصناها، وتنفيذ الشفرة بداخلها أولًا، ثم تغيير قيمة الخاصيّة:

استخدام الدالتين ()get و ()set معًا:

يمكننا وضع الدالتين تحت الخاصيّة، غض النظر عن الترتيب:

وعند القيام بتغيير قيمة الخاصيّة name أولًا، ثم طباعة قيمتها:

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

نلاحظ أنه، تم تنفيذ الشفرة في ()set أولًا، بالرغم من أننا كتبناها بعد ()get تحت الخاصيّة name. هذا لأننا في دالة ()main، قمنا بعملية إسناد قيمة لتغيير قيمة الخاصيّة، ثم في دالة ()println، استدعينا قيمة name.

عند الإسناد، بالرغم من أننا قمنا باستدعاء الخاصيّة name باستخدام النقطة، ولكن عندما يرى مترجم كوتلن جملة الإسناد البرمجية عبر العامل ( = )، سيستدعي مباشرةً الدالة ()set، إذا كتبناها تحت الخاصيّة. أمّا إذا لم نكتبها، سيستدعي الدالة الافتراضية. وإذا لم تكن هناك عملية إسناد سيستدعي دالة ()get، إذا كتبناها تحت الخاصيّة. أمّا إذا لم نكتبها، سيستدعي الدالة الافتراضية.

استخدام الخاصيّات بدلًا عن الدوال في الصنف:

كما رأينا في الفقرات أعلاه، تملك الخاصيّات حقل خاص بها، نتحكم فيه بدالتي ()get و ()set كما نريد. لذلك في بعض الأحيان، يكون استخدام الخاصيّات بدلًا عن الدوال للقيام بالوظائف البسيطة في الصنف، أكثر قابلية للقراءة.

فمثلًا، إذا كتبنا دالة داخل الصنف Student، لتعيد لنا معلومات الطالب، بعد القيام بإجراء واحد:

تتحقق دالة ()info باستخدام كتلة if الشرطية، مما إذا كان عمر الطالب يساوي 21 أو اكبر. إذا كانت نتيجة التعبير في if هي true، سيتم إعادة النص على يمينها، وفي حالة كانت نتيجة التعبير هي false، سيتم إعادة النص في else.

وحينما يتم استدعاء الدالة:

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

حينما تكون الدالة ليس بها معامل Parameter، ولديها عمل بسيط، مثل دالة ()info أعلاه، يمكننا تحويلها إلى خاصيّة:

تمكنّا من الإعلان عن الخاصيّة info دون تهيئتها (إضافة قيمة لها)، لوجود شفرة في الدالة ()get، تعيد قيمة. هذه القيمة، هي من سيتم إسنادها للخاصية. أمّا غير ذلك، فلن تسمح كوتلن بالإعلان عن الخاصيّات أو المتغيرات، دون تهيئتها، لتجنب مشكلة الـ null.

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

student.info

الخاصيّات الإضافية:

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

الإعلان عن خاصيّة إضافية:

يكون الإعلان عن خاصيّة إضافية، باستخدام الكلمة val، ثم اسم الصنف المُستقبِل المُراد إضافة الخاصيّة إليه Receiver Type، ثم نقطة ( . ) ثم اسم الخاصيّة ثم نقطتين متعامدتين وبعدهما نوع بيانات الخاصيّة. أمّا تحت سطر الخاصيّة، يجب أن نكتب دالة ()get:

val ReceiverType.extensionProperty: PropertyType

get() { … }

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

فمثلًا، إذا افترضنا أننا نحتاج إلى خاصيّة تكون قيمتها المِحرَف الأول من أي نص String، يمكننا إضافة هذه الخاصيّة إلى الصنف String:

val String.first: Char

    get() = this[0]

أضفنا خاصيّة first للصنف String ونوع بياناتها هو Char أي مِحرَف. لأن String هو عبارة عن سلسلة من المَحَارِف (حروف ورموز وأرقام …الخ) بين أقواس تنصيص مزدوجة ” “. وما نريده من first، أن تُمثل قيمة المِحرَف الأول من أي نص من النوع String.

وتتكون String من عدة كائنات من النوع Char، لدى كل منها فهرس يشير إليها، تمامًا مثل تجميعات القوائم Lists. لذلك، يمكننا الوصول إلى كل كائن فيها، عبر استخدام أقواس الفهرسة [ ]، ورقم الفهرس الذي يُمثله. ولأن الفهرس يبدأ دائمًا من الرقم صفر، إذًا المِحرَف الأول في أي نص String، سيكون فهرسه هو صفر.

في الشفرة أعلاه، نستخدم الكلمة this والتي تُمثِّل كائن الـ String الذي يستدعي الخاصيّة first. وعند إستدعاء هذه الخاصيّة مع أي كائن String:

fun main() {

    println(“ExVar”.first)

}

ستكون نتيجة الطباعة، هي المِحرَف الأول في الكائن:

التخلي عن الكلمة this:

كما نعرف من درس القوائم Lists، يمكننا استخدام دالة اسمها ()get نُرسل لها رقم فهرس العنصر كقيمة لمعاملها، وستعيد لنا قيمة العنصر نفسه (الدالة تختلف عن دالة الـ getter والخاصة بتغليف الخاصيّات ()get). هذه الدالة تتوفر تلقائيًا لأي تجميعة بها فهرس عددي، بالإضافة إلى النوع String.

فمثلًا، في مثالنا أعلاه يمكننا استخدامها بدلًا عن أقواس الفهرسة [ ]:

val String.first: Char

    get() = this.get(0)

هذه الشفرة ستقوم بنفس عمل السابقة. ولكن، لأن مترجم كوتلن ذكي كفاية ليفهم أننا نقصد الكائن الذي يستدعي first عند استخدام الدالة (0)get، يمكننا التخلي عن الكلمة this، وستعمل الشفرة أيضًا:

val String.first: Char

    get() = get(0)

إضافة خاصيّة للأصناف المُعَمَمّة Generic:

الأصناف المُعَمَمّة، هي الأصناف التي تقبل أي نوع بيانات كمعاملات أو عناصر. فمثلّا، إذا أردنا إضافة خاصيّة تعيد لنا العنصر الأول أو null من قائمة تحتوي على عناصر من أي نوع بيانات، يمكننا إنشاء قائمة List عامة، وإلحاق الخاصيّة بها:

نلاحظ أنه استدعينا دالة ()isEmpty بدون الكلمة this، لنفس السبب الذي شرحناه في الفقرة السابقة. وبالطبع يمكننا استبدال [0]this، بـ (0)get.

ستكون قيمة هذه الخاصيّة إمّا العنصر الأول من القائمة التي تستدعيها، أو null إذا كانت القائمة لا تحتوي على عناصر. ولأن نوع بيانات القائمة هو من العام T، ومن الممكن ألّا تحتوي القائمة على أية عناصر، كذلك يجب أن يكون نوع بيانات الخاصيّة هو النوع العام الذي يقبل قيم فارغة ?T، حتى نتجنب مشكلة الـ null.

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

استخدمنا الرمز n\ داخل نص دالة الطباعة، ليتم طباعة سطر جديد بعد طباعة النص. وعند تنفيذ الشفرة ستكون النتيجة:

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

استخدام النجمة ( * ) Star Projection:

عندما لا نحتاج إلى استخدام النوع عام T، أو إسناد قيمة من النوع عام إلى الخاصيّة، يمكننا استبداله بالنجمة ( * ) والتي تُعرف بـ Star Projection في كوتلن. فمثلًا، إذا ألحقنا خاصيّة بالنوع List، تُمثل قيمة نطاق أرقام الفهرس الخاصة بأي List:

ستُمكِّن النجمة الخاصيّة indices، من أن يتم استدعائها على قائمة تحتوي على عناصر من أي نوع بيانات. ولكننا لا نحتاج أن تكون الخاصيّة نفسها من النوع العام T، بل نوع محدد وهو نطاق عددي صحيح، أي من صفر إلى آخر رقم لفهرس القائمة. مثل (4..0) لقائمة بها خمس عناصر أو (9..0) لقائمة بها عشر عناصر. لذا استخدمنا الصنف IntRange من مكتبة كوتلن، ﻷنه مناسب تمامًا لهذه المهمة. (تفاصيل أكثر عن النطاقات، نجدها في درس for والنطاقات).

هنا أيضًا، تخلينا عن الكلمة this، عند استخدام الدالة ()isNotEmpty والخاصيّة size المتوافرتان للقائمة List افتراضيًا. الخاصيّة size كما نعرف من درس القوائم، تعيد عدد عناصر القائمة.

وعند إستدعاء الخاصيّة indices، ستعيد النطاق العددي لفهرس القائمة:

كان من الممكن كتابة الخاصيّة كالتالي:

ولكن نحتاج استخدام النوع العام T فقط في القائمة List وليس في الخاصيّة indices، لأن الخاصيّة لديها نوع محدد IntRange. لذا، نحذف النوع T، ونستبدله بالنجمة، والتي ستقوم بنفس عمل النوع العام T.

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

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