خاصية التمديد (Extension property) في Kotlin — ما هي، تركيبها واستخدامها

المؤلف: IT Sectr نُشر: 2026-06-21 وقت القراءة: 8 دق

extension property — آلية في Kotlin تسمح بإضافة خصائص جديدة إلى الفئات الموجودة دون وراثة ودون تعديل الكود المصدري. على عكس extension functions، لا يمكن لـ extension properties تخزين حالة — يتم تعريفها فقط مع getter، واختيارياً مع setter، لأنها لا تحتوي على backing field. وفقاً Kotlin Documentation, 2025، يتم ترجمة extension properties إلى طرق getter و setter ثابتة مع receiver كمعامل أول.

النقاط الرئيسية

  • Extension property — خاصية مع نوع مستقبل (receiver type)، يمكن الوصول إليها عبر تركيب خصائص Kotlin
  • لا يوجد backing field — لا يمكن لخاصية التمديد تخزين حالة، فقط حساب
  • Getter إلزامي، setter اختياري — يتم تعريفهما كدوال تمديد عادية
  • الأنواع القابلة للصفر مدعومة: يمكن أن يكون receiver قابلاً للصفر مع فحص داخلي
  • خاصية تمديد قابلة للتغيير — فقط عند تعريفها كـ var مع getter و setter

ما هي خاصية التمديد في Kotlin؟

خاصية التمديد هي بناء تركيبي في Kotlin يضيف خاصية إلى نوع موجود دون تغيير تعريفه. يتم تعريف الخاصية مع نوع مستقبل ويجب أن تحتوي على getter. الفرق الرئيسي عن الخصائص العادية هو عدم وجود backing field: لا يمكن لخاصية التمديد تخزين بيانات، فقط حسابها بناءً على كائن receiver.

وفقاً لاستطلاع Kotlin Foundation (2024)، فإن خصائص التمديد أقل شيوعاً من دوال التمديد — حوالي 45% من المطورين يستخدمونها بانتظام. يرجع ذلك إلى قيود عدم وجود حالة، مما يضيق نطاق التطبيق. ومع ذلك، بالنسبة للخصائص المحسوبة المرتبطة منطقياً بنوع، فإن خصائص التمديد هي الخيار الأكثر إيجازاً.

يتم ترجمة خصائص التمديد إلى زوج من طرق getter و setter الثابتة. على مستوى البايت كود، لا يوجد فرق بين استدعاء خاصية تمديد واستدعاء دالة تمديد — يصبح كلاهما طرقاً ثابتة مع معامل receiver. وفقاً لـ JetBrains (Kotlin Docs, 2025)، لا يوجد أي حمل إضافي.

استخدم خصائص التمديد للقيم المحسوبة القصيرة التي يجب أن تبدو كخصائص بدلاً من استدعاءات طرق — وهذا يحسن قابلية قراءة الكود ويلتزم بمبدأ الوصول الموحد.

تركيب خاصية التمديد: val و var

لتعريف خاصية تمديد، استخدم تركيباً مشابهاً لخاصية عادية، ولكن مع بادئة نوع المستقبل. val يعرف خاصية تمديد للقراءة فقط مع getter إلزامي، var يعرف خاصية قابلة للتغيير مع getter و setter اختياري.

kotlin
// خاصية تمديد للقراءة فقط
val String.isEmail: Boolean
    get() = this.contains("@") && this.contains(".")

// استدعاء
val valid = "test@test.com".isEmail

ملاحظة: يتم استدعاء خاصية التمديد بدون أقواس — str.isEmail، وليس str.isEmail(). هذا هو الفرق الرئيسي بين خاصية التمديد ودالة التمديد: الخاصية تبدو كحقل، على الرغم من أنها في الواقع تُحسب من خلال getter.

خاصية تمديد عامة (Generic)

يمكن أن تكون خصائص التمديد عامة — يمكن أن يستخدم receiver معاملات نوع عامة. وهذا يسمح بإنشاء خصائص عالمية تعمل مع أي نوع مجموعة.

kotlin
val List<T>.secondOrNull: T?
    get() = if (size >= 2) this[1] else null

val items = listOf("a", "b", "c")
val second = items.secondOrNull // "b"

خاصية secondOrNull تعمل لأي نوع T، وتعيد العنصر الثاني من القائمة أو null إذا كان هناك أقل من عنصرين. هذا مثال نموذجي حيث تكون خاصية التمديد أكثر ملاءمة من دالة — الوصول إليها يبدو كقراءة حقل.

لماذا لا يمكن لخاصية التمديد تخزين حالة

لا يمكن أن تحتوي خاصية التمديد على backing field لأنها لا تضاف إلى بيانات الفئة الوصفية — إنها موجودة فقط كزوج من دوال getter/setter ثابتة. Backing field (كلمة field المفتاحية في Kotlin) هو حقل داخلي للفئة يخزن قيمة الخاصية. لا تملك خاصية التمديد وصولاً إلى البنية الداخلية للفئة.

kotlin
// ❌ خطأ: لا يمكن أن تحتوي خاصية التمديد على backing field
var String.cachedValue: String
    get() = "computed"
    set(value) {
        field = value // الحقل غير قابل للوصول!
    }

// ✅ صحيح: استخدم تخزيناً خارجياً
val cache = MutableMap<String, String>()

var String.cachedValue: String
    get() = cache[this] ?: ""
    set(value) { cache[this] = value }

خريطة خارجية (Map) في المثال تحل مشكلة التخزين ولكنها تخلق مشكلة أخرى — تسرب الذاكرة. القيم التي تم الحصول عليها من خلال خاصية التمديد تعيش في الخريطة إلى الأبد إذا لم يتم تنظيفها. هذا القيد يجعل خصائص التمديد غير مناسبة للتخزين المؤقت أو تخزين البيانات المؤقتة.

للتخزين المؤقت، يوصى باستخدام WeakHashMap أو آليات مع التنظيف التلقائي. توصي JetBrains بتجنب استخدام var extension properties مع تخزين خارجي في كود الإنتاج دون إدارة دقيقة لدورة الحياة.

خاصية التمديد مقابل دالة التمديد: متى تختار ماذا

الاختيار بين خاصية التمديد ودالة التمديد يعتمد على الدلالات: الخاصية تصف خاصية من خصائص الكائن، بينما الدالة تصف إجراءً. مبدأ الوصول الموحد ينص على: لا يجب أن يعرف العميل ما إذا كانت القيمة محسوبة أو مخزنة. إذا كان يمكن تمثيل القيمة كخاصية (طول، حجم، حالة) — استخدم خاصية.

المعيارخاصية التمديددالة التمديد
الاستدعاءبدون أقواس: obj.propertyمع أقواس: obj.function()
الدلالةخاصية، سمةإجراء، عملية
Backing fieldغير مدعومغير قابل للتطبيق
المعاملاتفقط getter/setterأي معاملات
الأداءنفس الشيء (طريقة ثابتة)نفس الشيء (طريقة ثابتة)
مثالtext.lengthtext.isEmail()

القاعدة بسيطة: إذا كانت العملية تأخذ معاملات — استخدم دالة تمديد. إذا كانت قيمة محسوبة بسيطة بدون معاملات — استخدم خاصية تمديد. وفقاً لـ Android Architecture Guide (Google, 2025)، يجب إعطاء الأفضلية لخصائص التمديد للوصول إلى البيانات ودوال التمديد للعمليات ذات الآثار الجانبية.

خاصية تمديد قابلة للتغيير مع var و setter

خاصية التمديد مع الكلمة المفتاحية var تدعم setter، ولكن بدون القدرة على تخزين قيمة — عادةً ما يقوم setter بإجراء تأثير جانبي أو حفظ البيانات في مخزن خارجي. التركيب مشابه لخصائص الفئة القابلة للتغيير.

kotlin
// خاصية تمديد قابلة للتغيير مع setter
var StringBuilder.lastChar: Char
    get() = this[length - 1]
    set(value) {
        this.setCharAt(length - 1, value)
    }

val sb = StringBuilder("Kotlin")
println(sb.lastChar) // n
sb.lastChar = '!'
println(sb) // Kotli!

خاصية lastChar هي مثال كلاسيكي من توثيق Kotlin. getter يعيد آخر حرف من StringBuilder، setter يستبدله بقيمة جديدة. ملاحظة: الحالة مخزنة في StringBuilder نفسه (عبر setCharAt)، وليس في حقل منفصل — هذا استخدام صحيح لخاصية التمديد.

أمثلة عملية لخصائص التمديد

في المشاريع الحقيقية، تُستخدم خصائص التمديد غالباً لتبسيط الوصول إلى بيانات المجموعات، وحساب أحجام أو حالات عناصر واجهة المستخدم، وإنشاء واجهة برمجة تطبيقات مريحة فوق الفئات الموجودة. المكتبة القياسية لـ Kotlin تستخدم هذه الآلية بنشاط: size، indices، lastIndex للمجموعات هي خصائص تمديد.

kotlin
// خصائص التمديد للمجموعات
val List<Int>.sumFast: Int
    get() = fold(0) { acc, i -> acc + i }

val String.half: String
    get() = this.substring(0, length / 2)

// خاصية تمديد لـ Android View
val View.isVisible: Boolean
    get() = visibility == View.VISIBLE

// فحص القيمة الفارغة عبر مستقبل آمن
val String?.isNullOrBlank: Boolean
    get() = this == null || this.isBlank()

خاصية التمديد isVisible لـ View هي مثال يجب أن يعرفه كل مطور Android. بدلاً من view.visibility == View.VISIBLE، يمكنك كتابة view.isVisible. هذا ليس فقط أقصر، بل يُقرأ أيضاً كلغة طبيعية: «إذا كانت view مرئية». على الرغم من بساطتها، فإن هذه الخصائص تحسن بشكل كبير قابلية قراءة الكود.

الأسئلة الشائعة

هل يمكن تعريف خاصية تمديد لـ companion object؟

لا، لا يمكن تعريف خصائص التمديد لـ companion object أو object declaration. آلية التمديد تنطبق فقط على الفئات والواجهات والأنواع القابلة للصفر. لـ object، استخدم دوال عادية على المستوى الأعلى.

ما الفرق بين خاصية التمديد وخاصية inline؟

خاصية inline (مع معدل inline) هي آلية في Kotlin لاستدعاء getter/setter دون إنشاء كائن خاصية. خاصية التمديد تُترجم دائماً إلى طريقة ثابتة، بينما خاصية inline تُترجم إلى استدعاء بدون غلاف. تحل مشاكل مختلفة: خاصية التمديد تضيف خاصية إلى نوع موجود، بينما inline تحسن استدعاءات خصائصها الخاصة.

هل تدعم خصائص التمديد التعليقات التوضيحية (annotations)؟

نعم، يمكن أن تحتوي خاصية التمديد على تعليقات توضيحية، ولكن فقط على مستوى التعريف. لا يمكن وضع تعليق توضيحي منفصل على getter أو setter لخاصية تمديد — على عكس خصائص الفئة العادية. مثال: @JvmName("getIsValid") val String.isValid get() = true.

هل يمكن استخدام خاصية تمديد مع companion object لفئة؟

لا، لا يمكن تعريف خصائص التمديد مع companion object كـ receiver. هذا قيد لغوي — خاصية التمديد تعمل فقط مع مثيلات الأنواع، بينما companion object هو سياق ثابت. استخدم دوال تمديد على المستوى الأعلى أو ثوابت.

هل تؤثر خاصية التمديد على حجم APK؟

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

الخلاصة

  • خاصية التمديد — خاصية محسوبة لنوع موجود دون وراثة
  • لا يوجد backing field — لا يتم تخزين الحالة، فقط تُحسب عبر getter
  • var مع setter — ممكن، لكن يتطلب تخزيناً خارجياً لكتابة البيانات
  • التركيب — val/var مع receiver type و getter إلزامي
  • الأداء — بدون حمل إضافي، يُترجم إلى طريقة ثابتة
  • الاستخدام — خصائص محسوبة: الطول، الحالة، الحجم، الفحوصات
  • القيد — غير مناسب لتخزين الحالة، التخزين المؤقت دون إدارة ذاكرة

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا