فئة البيانات (Data class) في Kotlin: لماذا لا يمكن توريثها وكيفية تجاوز ذلك (تحليل تحت الغطاء)
\n\nهل سبق لك أن كتبت data class User(val name: String)، ثم أردت إنشاء data class Admin extends User؟ وحصلت على خطأ أحمر من المترجم؟ إذا كان الأمر كذلك، فأنت لست وحدك. سؤال «Extend data class in Kotlin» هو من بين الأسئلة الأكثر شيوعًا على Stack Overflow (أكثر من 50,000 مشاهدة).
اليوم لن نقدم لك إجابة جاهزة فقط. سننظر تحت غطاء Kotlin، ونشرح لماذا فرض مبتكرو اللغة هذا القيد «القاسي»، ونعرض 3 طرق قانونية لتجاوزه دون كسر البنية.
\n\nالمقالة مكتوبة لبوابة التعليم PythonLib – هنا نحب التعمق، وليس مجرد نسخ مقتطفات برمجية.
\n\nلماذا لا يمكن توريث فئة البيانات (data class)؟ نظرة على البايت كود
\n\nلنبدأ مباشرة. لنكتب فئة بيانات بسيطة ونرى ما يولده المترجم:
\n\ndata class User(val name: String, val age: Int)\n\nتحت الغطاء، يقوم Kotlin بإنشاء:
\n- \n
equals()– لمقارنة جميع الحقول \n hashCode()– تجزئة بناءً على جميع الحقول \n toString()– تمثيل نصي جميل \n copy()– دالة نسخ مع إمكانية تغيير الحقول \n componentN()– التفكيك (Destructuring) \n
والآن تخيل أنك تريد توريث فئة بيانات:
\n\nopen data class Base(val x: Int)\ndata class Derived(val x: Int, val y: Int) : Base(x) // خطأ في الترجمة!\n\nيُصدر Kotlin: «Inheritance of data classes is not allowed». لماذا؟ دعنا نفهم.
\n\nالمشكلة رقم 1: copy() تتعطل. دالة copy() تُنشأ فقط لحقول الفئة الحالية. إذا كان لديك Derived الموروث من Base، فإن copy() في Base لا تعرف عن الحقل y. باستدعاء base.copy()، ستفقد بيانات الوريث.
المشكلة رقم 2: equals() و hashCode(). تخيل: Base(1) و Derived(1, 2). هل يجب أن يكونا متساويين؟ إذا كانت الإجابة بنعم – يتم انتهاك التناظر (لأن Derived يحتوي على حقول أكثر). إذا كانت الإجابة لا – يتم انتهاك عقد equals/hashCode. اختار Kotlin الطريق الآمن – منع التوريث.
المشكلة رقم 3: componentN(). val (x) = base و val (x, y) = derived – عدد مختلف من المكونات. في حالة التوريث، سينشأ ارتباك.
الخلاصة: فئة البيانات (data class) صُممت لحاويات البيانات غير القابلة للتغيير، وليس للتسلسلات الهرمية. هذا قرار واعٍ في تصميم اللغة.
\n\nالطريقة رقم 1: التركيب (Composition) بدلاً من التوريث (موصى به)
\n\nأنظف طريقة هي عدم التوريث، بل تضمين فئة بيانات داخل أخرى. هذا يسمى التركيب (Composition).
\n\ndata class Address(val city: String, val street: String)\n\ndata class User(\n val name: String,\n val age: Int,\n val address: Address // تركيب\n)\n\nfun main() {\n val address = Address("موسكو", "تفيرسكايا")\n val user = User("آنا", 25, address)\n println(user) // User(name=آنا, age=25, address=Address(city=موسكو, street=تفيرسكايا))\n \n // النسخ يعمل بشكل مثالي\n val user2 = user.copy(age = 26)\n println(user2) // User(name=آنا, age=26, address=Address(city=موسكو, street=تفيرسكايا))\n}\n\n\nالإيجابيات: كل شيء يعمل «فوريًا»، لا مفاجآت مع equals/hashCode، الكود شفاف.
\n\nالسلبيات: إذا كنت بحاجة إلى مستويات متعددة من التداخل، ستحصل على «دمية ماتريوشكا». ولكن يمكن حل ذلك بتطبيع البيانات.
\n\nالطريقة رقم 2: الفئة المختومة (Sealed class) + فئة البيانات (data class) (للتسلسلات الهرمية الصارمة)
\n\nإذا كنت تحتاج حقًا إلى تسلسل هرمي (مثل أنواع مختلفة من المستخدمين)، استخدم sealed class (أو sea