تعلّم البرمجة بلغة كوتلن (38): أصناف البيانات Data Classes

تعلّم البرمجة بلغة كوتلن (38): أصناف البيانات Data Classes
أستمع الى المقال

حتى الآن في هذه الدورة لتعلم كوتلن، شرحنا كيف يمكننا استخدام الكائنات Objects في البرمجة عبر إنشاء الأصناف Classes. هذه الأصناف يمكن أن تحتوي على خاصيات Properties في الباني الأساسي Primary Constructor، أو يمكن أن نضع بها دوال تقوم بوظائف مختلفة، أو حتى أن نلحق دوال إضافية بالأصناف التي لا نملك إمكانية تحريرها وتعديلها.

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

استخدام صنف خاص لحفظ بيانات كائن:

كمثال، فلنقم بإنشاء برنامج يقرأ بيانات مجموعة من التلاميذ، تتكون من إسم التلميذ name ورقمه التعريفي id فقط. ولأنه يمكن لمُدخِل هذه البيانات أن يخطئ ويدخل بيانات أي تلميذ مرتين، يمكننا حفظ هذه البيانات في تجميعة من النوع Set، لأنها تمنع التكرار. 

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

تم شرح معظم أجزاء الشفرة أعلاه بالتفصيل، في درس الخرائط Maps. البرنامج يتكون من الدالة الرئيسية ()main وصنف خاص Student لديه خاصيّتان id, name. في دالة ()main، نبدأ الشفرة برسالة للمستخدم تخبره أن يدخل الرقم التعريفي id، والإسم name، مع فصلهما بفاصلة (,)، وإدخال الكلمة “stop” عند الإنتهاء.

ننشئ تجميعة Set قابلة للتغيير عبر استخدام دالة ()mutableSetOf، ونحدد لها أنواع العناصر من نوع الصنف الخاص بنا Student. ونسندها إلى متغير أسميناه students.

وفي حلقة while، بعد قراءة المدخلين وحفظهما في متغيرين studentId, .studentName ننشئ كائن من الصنف Student، ونمرر لباني الصنف المتغيرين. ونسند الكائن إلى متغير أسميناه student.

لابد من تحويل نوع المتغير studentId إلى النوع Int عبر استخدام دالة ()toInt من مكتبة كوتلن القياسية. هذا التحويل ضروري، لأن دالة ()readln، تقرأ المدخلات وتعيدها لنا من النوع String، في حين أن الخاصيّة id في الصنف Student من النوع Int.

بعد قراءة المدخلات، واستخدامها في إنشاء كائن Student، نتحقق عبر كتلة if ما إذا كان الكائن موجود مسبقًا في تجميعة students أم لا، عبر استخدام دالة ()contains. إذا كان الكائن متواجد في التجميعة، سنطبع رسالة تنبه المستخدم لذلك، أما غير ذلك، سنضيف الكائن الجديد إلى التجميعة students عبر استخدام دالة ()add. وفي كل الأحوال، ستواصل حلقة while تكرار طلب القراءة، والتحقق عبر كتلة if، حتى يدخل المستخدم الكلمة stop، حينها ينتهي عملها.

وفي نهاية الشفرة، نطبع التجميعة عبر دالة ()println:

كما نرى في الصورة، أدخلنا ثلاث مدخلات وتم حفظها في تجميعة students. ولكن عناصر التجميعة ظهرت كالتالي:

[Student@7cca494b, Student@7ba4f24f, Student@3b9a45b3]

ليس هذا ما كنا ننتظره، لأن كائن الـ Student يتكون من اسم ورقم تعريفي. لطباعة الكائنات في التجميعة بطريقة مفهومة لنا، لا بد من تطبيق دالة ()toString في صنفنا، والتي تتواجد بالصنف الأب لكل الأصناف في كوتلن، الصنف Any. 

الصنف Any:

هذا الصنف هو بمثابة الصنف الأب superclass لكل أصناف كوتلن، بما فيها الأصناف الخاصة بنا. كل ما يحمل الكلمة المفتاحية class، يَرِث من الصنف Any خاصيّاته ودواله. (لن نغوص كثيرًا في الوراثة في هذا الدرس لأننا سنشرحها تفصيليًا في قسم البرمجة الكائنية).

ولكن عمومًا، لدى هذا الصنف ثلاث دوال نحتاج إلى تطبيقها في أصنافنا الخاصة. حينما نريد استخدام هذه الأصناف، كـ أصناف بيانات Data Classes، كما في حالة الصنف Student. هذه الدوال هي: ()toString، و ()equals، و ()hashCode.

دالة ()toString:

مهمة هذه الدالة هي إعادة الشكل النصي لأي كائن في كوتلن، عند استخدامها معه. ولنتمكن من استخدامها مع كائن من النوع Student في مثالنا أعلاه، لابد أولًا من عمل override لها داخل الصنف Student. أي بمعنى استعارتها من الصنف الأب Any وإعادة تعريفها كما نريد:

عدّلنا الصنف Student وأضفنا الدالة عبر استخدام الكلمة المفتاحية override. وهي الكلمة التي لابد من كتابتها قبل الدوال التي نريد تطبيقها من أي صنف يُمثِّل أب لصنفنا. لا يمكننا تغيير اسم الدالة ولا نوع الإرجاع فيها، ولكن يمكننا تغيير القيمة التي تعيدها، والتي لابد أن تكون من النوع String، تمامًا مثل الدالة في الصنف الأب.

لذلك جعلنا الدالة تعيد النص:

 “Student(id = $id name = $name)” 

لأننا نريد من النص أن يكون واضحًا أنه آتٍ من الكائن Student، استخدمنا اسم الصنف في النص. ثم رتبنا طباعة الخاصيّتين كما نريد.

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

مقارنة بيانات الكائنات:

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

This student is already in the list

لنجرب الإدخال المكرر لنتأكد من أن ذلك ما سيحدث:

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

دالة ()equals:

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

لذا، يجب أن نستعير أيضًا دالة ()equals من الصنف Any، وتطبيقها في صنف Student الخاص بنا:

عدّلنا الصنف مرة أخرى وطبقنا دالة ()equals عبر استخدام الكلمة override. تقوم الشفرة داخل الدالة، بمقارنة كائني Student بعدة طرق منها: استخدام خاصيتي الإسم name والرقم التعريفي id.

ولكن حتى بعد إضافة دالة equals، لن تعمل المقارنة كما يجب، لأننا نحتاج أيضًا لتطبيق دالة أخرى من الصنف الأب Any، وهي دالة ()hashCode. وسنعرف السبب في الفقرة أدناه.

دالة ()hashCode:

فكرة الـ hash code هي وضع كل كائن في مكان خاص به مع تمييزه برقم من النوع Int، ويكون هو الهوية الخاصة بهذا الكائن. تم تطبيق الـ hash-code في تجميعات مثل: HashMap and HashSet. بالطبع سنشرح ذلك تفصيليًا، لاحقًا في هذه الدورة.

عطفًا على ذلك، يمكننا القول أنه يجب أن يكون للكائنات المتساوية نفس رمز الـ hash-code في برنامج ما أثناء تشغيله. ولمعرفة رقم الـ hash لأي كائن، يمكننا أن نستخدم معه دالة ()hashCode. ولأن هذه الدالة متواجدة بالصنف الأب Any، فيمكن لأي كائن في كوتلن استخدامها أو تطبيقها داخله عبر الكلمة override.

لذا، إذا تساوت بيانات كائنين وتساوت أرقام الـ hash الخاصة بهما، فهما نفس الكائن. طبّقنا دالة ()equals مسبقًا، والآن سنطبّق دالة ()hashCode في صنف Student:

كما نرى في الشفرة، أن كل عمل دالة ()hashCode، هو إعادة رقم الـ hash الخاص بالكائن الذي تم تطبيقها بداخله، وهو رقم من النوع Int. لإعطاء الكائن رمز متفرد خاص به، ضربنا الرقم 31 في قيمة الخاصية id الخاصة بالصنف Student. ثم جمعنا الناتج بقيمة رمز الـ hash للخاصيّة name. ناتج هذه العملية، هو رمز الـ hash الخاص بـ Student. الخاصيّة name كائن من النوع String، لذا لديها hash-code خاص بها.

الآن بعد تطبيق هاتين الدالتين، فلنحاول الآن مرة أخرى:

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

التجميعة Set تتجاهل التكرار:

في هذه المرحلة من الشفرة، وإذا لم نكن نريد إظهار رسالة تنبيه للمستخدم، يمكننا حذف كتلة if، ودالة ()contains:

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

تعرفت الـ Set على كائنات الـ Student المكررة، لأن الصنف Student يستخدم دالتي ()equals و ()hashCode. واللتان تحددان متى يجب أن يُعتبر كائنان متساويان عند مقارنتهما.

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

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

هذا هو الحال في لغات مثل لغة البرمجة المخضرمة جافا (صنف البيانات، يسمى pojo في جافا). بالطبع توجد بعض الحلول لهذا الأمر، كأن نجعل برنامج IntelliJ ينتج لنا شفرات الدالتين تلقائيًا. ولكن ما تزال هناك مشكلة الـ boilerplate code، أي كتابة تعليمات برمجية كثيرة وفي أماكن متعددة، للقيام بوظائف بسيطة ومتشابهة. وشفرات كثيرة يعني أخطاء أكثر وقابلية قراءة أقل.

لتجنب كل ذلك وتسهيل مهمة استخدام هذا النوع من الأصناف، قدمت لنا كوتلن صنف البيانات Data Class. كل ما سنفعله، هو كتابة الكلمة المفتاحية data قبل كلمة class، ونترك باقي المهمة لكوتلن. ليصبح شكل الصنف Student، كالتالي:

data class Student(private val id: Int, private val name: String)

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

تم طباعة الكائنات بطريقة نصية مفهومة، وهذا دليل على أنه تم تطبيق دالة ()toString. وأيضًا، تعرفت تجميعة الـ Set على الكائنات المكررة وتخلصت منها، وهذا دليل على وجود دالتي ()equals و ()hashCode.

الخاصيّات التي يتم الإعلان عنها في جسم الصنف:

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

data class Student(private val id: Int, private val name: String) {

    val age = 0

}

لن يستخدم المترجم الخاصيّة age عند إنشائه الدوال الثلاث تلقائيًا.

تطبيق الدوال الثلاث في صنف data:

فمثلًا، إذا عملنا override لدالة ()toString داخل صنف البيانات، سيستخدم المترجم هذه الدالة بدلًا عن تلك التي ينشئها تلقائيًا:

data class Student(private val id: Int, private val name: String) {

    val age = 0

    override fun toString() = “Student age = $age”

}

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

ولكن هل هذا كل ما تفعله الكلمة data؟ لا، بجانب هذه الدوال الثلاث، يتم تطبيق دالة ()copy أيضًا.

دالة ()copy:

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

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

شروط إنشاء صنف بيانات:

من أجل إنشاء صنف بيانات، يشترط تلبية المتطلبات التالية:

  • يجب وضع خاصية واحدة على الأقل لباني الصنف الأساسي.
  • يجب الإعلان عن كل الخاصيات ب val أو var.
  • لا يمكن الإعلان عنه كـ abstract, open, sealed, or inner. (سندرس كل هؤلاء في الدروس القادمة في هذه الدورة).

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

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