تخطَّ إلى المحتوى

الكلاسات في Kotlin: class و data class والوراثة

الدرس 7 من 11 · Kotlin

تطبيقات أندرويد مليئة بـ «أشياء»: مستخدم، ورسالة، ومنتج، وطلب. وكل شيء منها له بيانات (اسم، سعر) وسلوك (يرسل، يُحذف). الطريقة المنظمة لتمثيل هذه الأشياء هي الكلاسات. وفي Kotlin، كتابة الكلاسات أقصر بكثير من Java، وفيها ميزة رائعة اسمها data class ستوفر عليك كوداً كثيراً.

class Student(val name: String, var age: Int) {
fun introduce() {
println("أنا $name وعمري $age سنة")
}
}
fun main() {
val s1 = Student("سارة", 14)
val s2 = Student("عمر", 15)
s1.introduce()
s2.introduce()
s2.age = 16
println(s2.age)
}
الناتج
أنا سارة وعمري 14 سنة
أنا عمر وعمري 15 سنة
16

السطر الأول وحده يفعل الكثير:

  • class Student: تعريف الكلاس، واسمه بحرف كبير.
  • (val name: String, var age: Int): المُنشئ الأساسي (Primary Constructor)، وهو في نفس الوقت يعرّف الخصائص. val خاصية للقراءة فقط، و var قابلة للتعديل.
  • لإنشاء كائن تكتب اسم الكلاس مع القيم: Student("سارة", 14). لا توجد كلمة new في Kotlin.

الكلاس مثل قالب الكعك، وكل كائن كعكة مستقلة: تغيير عمر عمر لم يغيّر عمر سارة.

الكتلة init تُنفَّذ عند إنشاء كل كائن، وهي مكان مناسب للتحقق من صحة البيانات:

class Product(val name: String, val price: Double, val quantity: Int = 1) {
val total: Double
get() = price * quantity
init {
require(price >= 0) { "السعر لا يمكن أن يكون سالباً" }
}
}
fun main() {
val p = Product("كتاب", 60.0, 3)
println(p.total)
println(Product("قلم", 5.0).total)
try {
Product("خطأ", -1.0)
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
الناتج
180.0
5.0
السعر لا يمكن أن يكون سالباً
  • require(شرط) { رسالة }: إذا كان الشرط خطأ، يرمي استثناءً IllegalArgumentException بالرسالة، فلا يُنشأ منتج بسعر سالب أبداً.
  • get() = ...: خاصية محسوبة تُحسب كلما قرأتها، ولا تُخزَّن.
  • المعامل quantity له قيمة افتراضية، تماماً مثل الدوال.

أحياناً تريد أن يقرأ الجميع الخاصية، لكن لا يغيّرها إلا الكلاس نفسه:

class BankAccount(val owner: String) {
var balance = 0.0
private set
fun deposit(amount: Double) {
if (amount <= 0) {
println("المبلغ يجب أن يكون أكبر من صفر")
return
}
balance += amount
}
}
fun main() {
val acc = BankAccount("منى")
acc.deposit(500.0)
acc.deposit(-20.0)
println(acc.balance)
}
الناتج
المبلغ يجب أن يكون أكبر من صفر
500.0

private set تعني: القراءة متاحة للجميع، أما التعديل فمن داخل الكلاس فقط. لو كتبت خارجه acc.balance = 1000000.0 سيرفض المترجم. والطريقة الوحيدة لزيادة الرصيد هي deposit التي تتحقق من المبلغ. هذا هو التغليف (Encapsulation).

كثير من الكلاسات وظيفتها حمل البيانات فقط، مثل كتاب أو رسالة. لهذه الكلاسات أضف كلمة data:

data class Book(val title: String, val pages: Int)
fun main() {
val b1 = Book("البرمجة للمبتدئين", 250)
val b2 = Book("البرمجة للمبتدئين", 250)
println(b1)
println(b1 == b2)
val b3 = b1.copy(pages = 300)
println(b3)
}
الناتج
Book(title=البرمجة للمبتدئين, pages=250)
true
Book(title=البرمجة للمبتدئين, pages=300)

بكلمة واحدة حصلت على ثلاث هدايا:

  1. طباعة مفهومة: بدلاً من شيء مثل Student@77459877 الذي يُطبع للكلاس العادي.
  2. مقارنة بالمحتوى: كتابان بنفس البيانات متساويان بـ ==.
  3. copy: نسخة جديدة مع تعديل خاصية أو أكثر، والأصل يبقى كما هو.

في تطبيقات أندرويد ستستخدم data class لكل البيانات القادمة من الإنترنت أو قاعدة البيانات.

في Kotlin، الكلاسات مغلقة افتراضياً: لا يمكن الوراثة منها إلا إذا سمحت بذلك بكلمة open:

open class Animal(val name: String) {
open fun speak() {
println("$name يصدر صوتاً")
}
}
class Cat(name: String) : Animal(name) {
override fun speak() {
println("$name يقول: مياو")
}
}
fun main() {
val animals = listOf(Animal("الحصان"), Cat("مشمش"))
for (a in animals) a.speak()
}
الناتج
الحصان يصدر صوتاً
مشمش يقول: مياو
  • open class: تسمح بالوراثة من الكلاس، و open fun: تسمح بإعادة كتابة الدالة.
  • : Animal(name): القطة ترث من الحيوان، وتمرر الاسم لمُنشئه.
  • override: إعادة كتابة الدالة الموروثة بسلوك خاص.

لماذا الإغلاق افتراضياً؟ لأن الوراثة غير المخطط لها تسبب أخطاء صعبة. Kotlin تجبرك على التفكير: هل صممت هذا الكلاس فعلاً ليُورَث؟

  1. اكتب data class Course فيها: الاسم، وعدد الدروس، والحالة isFree بقيمة افتراضية true.
  2. أنشئ قائمة من 3 كورسات، واطبعها.
  3. استخدم filter لتطبع الكورسات التي دروسها أكثر من 8.
  4. تحدٍّ: اكتب open class Shape بدالة area() تُرجع 0.0، وكلاسين يرثان منها: Circle و Rectangle، كل منهما يعيد كتابة area. ثم اطبع مساحات قائمة أشكال مختلفة.
  • الوراثة من كلاس غير open: خطأ This type is final, so it cannot be extended. أضف open.
  • نسيان override: إعادة كتابة دالة موروثة بدون override يرفضها المترجم.
  • استخدام class عادي لحمل البيانات: تفقد الطباعة المفهومة والمقارنة و copy. استخدم data class.

سؤال

ما الفائدة الأساسية من data class؟

سؤال

لماذا يرفض المترجم class Dog : Animal() إذا كان Animal معرّفاً بـ class Animal؟

  • class Student(val name: String, var age: Int) يعرّف الكلاس وخصائصه ومُنشئه في سطر واحد.
  • init و require للتحقق من البيانات، و get() للخصائص المحسوبة.
  • private set يسمح بالقراءة ويحمي التعديل من خارج الكلاس.
  • data class لحمل البيانات: طباعة مفهومة، ومقارنة بالمحتوى، و copy.
  • الوراثة تحتاج open في الأب، و override لإعادة كتابة الدوال.