الخاصّيّات المُعمَّمة (Delegated Properties) في لغة Kotlin

من موسوعة حسوب
مراجعة 11:34، 30 أغسطس 2018 بواسطة عبد اللطيف ايمش (نقاش | مساهمات) (استبدال النص - 'Kotlin Properties' ب'Kotlin Property')
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)

 استخدام الخاصّيّات المُعمَّمة

تستطيع في لغة Kotlin تعريف استخدام (implement) الخاصّيّات يدويًا مرارًا وتكرارًا بكل مرةٍ تحتاجها، ولكن من الأسهل تعريف استخدامها مرةً واحدةً وتخزين هذا التعريف في المكتبة (library) للاستفادة منه كلما دعت الحاجة، وهذا يشمل:

  • الخاصّيّات الكسولة (Lazy property): تُحسب قيمتها مرةً واحدةً فقط وذلك عند الوصول إليها للمرّة الأولى.
  • الخاصّيّات المُراقَبة (observable property): إذ يُستدعَى مسؤول الانتظار (listener) عند حدوث أي تغييرٍ في الخاصّيّة.
  • تخزين الخاصّيّات في map بدلًا من حقلٍ منفصلٍ لكلِّ منها.

وتشمل لغة Kotlin كلّ تلك الحالات بدعمها للخاصّيّات المُعمَّمة (delegated properties) بالصيغة العامّة الآتية:

val/var <property name>: <Type> by <expression>

كما في الشيفرة الآتية:

class Example {
    var p: String by Delegate()
}

إذ إن التعبير الواقع بعد الكلمة المفتاحيّة by سيكون هو المُعمَّم (delegate) لأن دالتيّ get()‎ و set()‎ ستُعمَّمان للدالتين getValue()‎ و setValue()‎ الموافقتين لهما، وبالتالي لا حاجة لتعريف استخدام (implement) أيّ واجهةٍ، بل يكفي وجود الدالة getValue()‎ (والدالة setValue()‎ في حالة المتغيِّرات من النوع var)، مثل الشيفرة الآتية:

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }
 
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

فعند قراءة القيمة من p والمُعمَّمة لأي كائن (instance) من Delegate تُستدعَى الدالة getValue()‎ من Delegate حيث يكون المعامل (parameter) الأوّل فيها هو الكائن الذي تٌقرَأ منه قيمة p ويحتوي الثاني على وصفٍ لها (للحصول على اسمها مثلًا)، مثل:

val e = Example()
println(e.p)

وهذا سيُظهر النتيجة:

Example@33a17727, thank you for delegating ‘p’ to me!

وبشكلٍ مشابهٍ؛ تُستدعَى الدالة setValue()‎ عند الإسناد للمتغيِّر p، حيث يتماثل المعاملان الأول والثاني بينما يحتوي الثالث على القيمة المُسندَة، فعند تنفيذ الشيفرة الآتية:

e.p = "NEW"

ستظهر النتيجة:

NEW has been assigned to ‘p’ in Example@33a17727.

وسيأتي شرح متطلَّبات القيام بمثل هذا التعميم في فقرةٍ لاحقةٍ في الصفحة الحاليّة.

ملاحظة: أصبح بالإمكان بدءًا من الإصدار Kotlin 1.1 التصريحُ عن الخاصّيَة المُعمَّمة داخل الدالة أو جزءٍ من الشيفرة (code block) ولا يُشترَط أن تكون عنصرًا (member) في الصنف، وستجد مثالًا عنها تحت عنوان الخاصيات المُعمَّمة المحليّة في هذه الصفحة.

التعميمات القياسيّة (Standard Delegates)

تحتوي مكتبة Kotlin القياسيّة على عددٍ من التوابع المُنتِجة (factory methods) لأنواع التعميمات المختلفة، وهي:

الكسولة (Lazy)

تأخذ الدالة lazy()‎ تعبير lambda وتعيد كائنًا (instance) من النوع Lazy<T>‎ المُعمَّم لتعريف استخدام الخاصية الكسولة، إذ تُحسَب قيمة تعبير lambda المُمرَّر للدالة عند الاستدعاء الأول (أي عند استخدام get()‎) لتُستخدَم نفسُ القيمة لكافّة الاستدعاءات اللاحقة، مثل:

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)
    println(lazyValue)
}

سينتُج عن تنفيذ الشيفرة السابقة النتيجة:

computed!
Hello
Hello

وتُعدُّ عملية حساب الخاصّيّة الكسولة متزامنةً (synchronized) حيث تُحسَب في thread واحدٍ وستحصل كل threads الأخرى نفس القيمة، وإن لم يكن مهمًا أن تكون عملية التهيئة متزامنةً سيُسمح القيام بها بأكثر من thread بنفس الوقت وحينئذٍ يجب تمرير LazyThreadSafetyMode.PUBLICATION للمعامل الأول في الدالة lazy()‎ إذ يُستخدَم النمط LazyThreadSafetyMode.NONE عندما يُضمَن أنّ عملية التهيئة لن تحدث بأكثر من thread.

المُراقَبة (Observable)

تحتوي الدالة Delegates.observable()‎ على وسيطين (arguments) وهما: القيمة الأوليّة (initial value) ومسؤول (handler) التعديلات، إذ يُستدعَى الوسيط الثاني بكلّ مرّةٍ يجري فيها إسنادٌ للخاصّيّة (بعد عمليّة الإسناد لا قبلها)، وتحتوي الدالة في بُنيتها (body) على ثلاثة معاملاتٍ: الخاصّيّة التي تُسنَد القيمة إليها والقيمة السابقة والقيمة الجديدة، كما في الشيفرة الآتية:

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "first"
    user.name = "second"
}

والتي سينتُج عنها:

<no name> -> first
first -> second

ويُستفادُ في بعض الأحيان من الدالة ()vetoable بدلًا من observable()‎ ، حيث سيُستدعَى المسؤول (handler) المُمرَّر للدالة ()vetoable قبل تنفيذ عملية الإسناد للخاصّيّة.

تخزين الخاصّيّات في Map

يُستفاد من هذه الميّزة في التطبيقات الديناميكيّة أو في تحليل JSON‏ ( parsing JSON)، وفي هذه الحالة يُستخدَم الكائن map نفسه كنوعٍ مُعمَّمٍ للخاصّيّة، مثل:

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

ففي الشيفرة السابقة يحتوي الباني (constructor) على map بالشكل الآتي:

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

إذ تحصل الخاصّيّات المُعمَّمة على القيم من map عبر المفاتيح (key) والتي هي أسماء الخاصيِّات كما في الشيفرة:

println(user.name) // ستظهر العبارة "John Doe"
println(user.age)  // ستظهر القيمة 25

وهذا يصلُح أيضًا للخاصّيّات من النوع var عبر استخدام MutableMap بدلًا من Map المُعدَّة للقراءة فقط (read-only)، لتصبح الشيفرة بالشكل الآتي:

class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

الخاصيات المُعمَّمة المحليّة (بدءًا من الإصدار 1.1)

تسمح لغة Kotlin بالتصريح عن المتغيِّرات المحليّة (local variables) كخاصيّات مُعمَّمة؛ كجعل المتغير المحليّ من النوع الكسول (lazy)، مثل:

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

عندها ستُحسَب قيمة المتغيِّر memoizedFoo  عند الوصول إليه للمرة الأولى فقط، وإذا حدث أيّ خللٍ في الشرط someCondition فلن تُحسَب مطلقًا.

متطلبات تعميم الخاصيات (Property Delegate Requirements)

  • إن كانت الخاصّيّة مُعدَّة للقراءة فقط (read-only) أي أنّها من النوع val فيجب أن يحتوي التعميم منها على الدالة getValue()‎ ولها المعاملات:
    • thisRef : وله نفس نوع الخاصّيّة الأصليّة أو أيّ نوعٍ أعلى (supertype)، وفي حالة الخاصّيّات الإضافيّة (extension properties) النوع المُوسَّع (extended).
    • property : وله النوع KProperty<*>‎ أو نوعه الأعلى.

وبإمكان هذه الدالة أن تعيد نفس النوع المُستخدَمِ في الخاصّيّة (أو أنواعها الفرعيّة [subtypes]).

  • أمَا إن كانت الخاصّيّة متغيّرة (mutable) أي أنّها من النوع var فيجب أن تحتوي أيضًا على الدالة setValue()‎ ولها المعاملات:
    • thisRef: كما في الدالة getValue()‎
    • property: كما في الدالةgetValue()‎
    • القيمة الجديدة: ويجب أن تكون من نفس نوع الخاصّيّة أو أيّ نوع أعلى (supertype).

وتكون الدالتان السابقتان (getValue()‎ و setValue()‎) إمّا كعناصر في صنف التعميم (delegate class) أو كدوال إضافيّة (extension functions) حيث تُستخدَم الحالة الثانية عند تعميم خاصّيّة لكائنٍ (object) لا يحتوي بالأصل على هذه الدوال، إذ يجب استخدام الكلمة المفتاحيّة operator قبل أيّ منهما.

ومن الممكن أن يحتوي صنف التعميم على تعريف استخدام (implement) لأيّ من الواجهتين ReadOnlyProperty و ReadWriteProperty واللتان تحتويان على توابع operator المطلوبة، إذ إنّ الواجهتين مُعرَّفتان في المكتبة القياسيّة في Kotlin كما يلي:

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

قواعد الترجمة (Translation Rules)

يُولِّد المُترجِم (compiler) خاصّيّةً مساعدةً (auxiliary property) لكلّ خاصّيّة مُعمَّمة، فتُولَّد للخاصية prop مثلًا الخاصّيّة المخفيّة prop$delegate وتُعمَّم شيفرةُ الوصول لهذه الخاصّيّة الإضافيّة لتصبح بالشكل:

class C {
    var prop: Type by MyDelegate()
}

// تُولَّد هذه الشيفرة من قِبل المترجم
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

إذ يزوِّد المترجِم في لغة Kotlin بكافّة المعلومات اللازمة عن الخاصّيّة prop من خلال الوسائط (arguments) وهي: this والذي يشير إلى كائن (instance) الصنف الخارجيّ C و this::prop والذي هو كائن انعكاسيّ (reflection object) من النوع KProperty  الذي يصِف الخاصية prop نفسها.

ملاحظة: إنّ الصيغة this::prop (والتي تُستخدَم للإشارة إلى المرجعيّة المرتبطة القابلة للاستدعاء [ bound callable reference] في الشيفرة مباشرةً) مُتاحةٌ بدءًا من الإصدار Kotlin 1.1.

التزويد بالتعميم (delegate) (بدءًا من الإصدار 1.1) 

من الممكن توسعة (extend) منطق إنشاء الكائنات الذي يُعمَّمُ له تعريف استخدام (implementation) الخاصّيّات عبر تعريف provideDelegate ، فإذا احتوى الكائن (المُستخدَم على الجانب الأيمن من by) على الدالة provideDelegateكدالةٍ في الصنف أو دالةٍ إضافيةٍ (extension)، فستُستدعَى هذه الدالة لإنشاء كائن تعميم الخاصّيّة (the property delegate instance)، إذ تفيد هذه الدالة في حالة التحقُّق من توافق الخاصّيّة (property consistency) عند إنشائها وليس فقط عند استخدامها (سواءً عبر getter أو setter).

فللتحقُّق مثلًا من اسم الخاصية قبل تقييدها (binding)، تصبح الشيفرة:

class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
    override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}
    
class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        //  إنشاء التعميم
        return ResourceDelegate()
    }

    private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

class MyUI {
    fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

وتحتوي الدالة provideDelegate()‎ (كما هو الحال في دالة getValue()‎) على المعاملات:

  • thisRef : وله نفس نوع الخاصّيّة الأصليّة أو أيّ نوعٍ أعلى (supertype)، وفي حالة الخاصّيّات الإضافيّة (extension properties) النوع المُوسَّع (extended).
  • property : وله النوع KProperty<*>‎ أو نوعه الأعلى.

وتُستدعى الدالة لكل خاصّيّة عند عملية إنشاء الكائن (instance) من MyUI وتُنفِّذ مباشرةً عملياتِ التحقُّق اللازمة.

وبدون تلك المقدرة على تحقيق الصلة ما بين الخاصّيّة وتعميمها، يجب تمرير اسم الخاصّيّة بشكلٍ صريحٍ لإنجاز نفس المهمة السابقة، ويصعُب ذلك كما في الشيفرة الآتية:

// التحقق من اسم الخاصيات دون الاعتماد على
// provideDelegate الدالة

class MyUI {
    val image by bindResource(ResourceID.image_id, "image")
    val text by bindResource(ResourceID.text_id, "text")
}

fun <T> MyUI.bindResource(
        id: ResourceID<T>,
        propertyName: String
): ReadOnlyProperty<MyUI, T> {
   checkProperty(this, propertyName)
   // إنشاء التعميم
}

إذ يُستدعَى التابع provideDelegate في الشيفرة المُولَّدة لتهيئة الخاصّيّة المساعدة prop$delegate. قارن بين الشيفرة الآتية المُولَّدة لتصريح الخاصّيّة: val prop: Type by MyDelegate()‎ والشيفرة المُولَّدة سابقًا (والمشروحة في فقرة قواعد الترجمة حيث لا وجود للتابع provideDelegate):

class C {
    var prop: Type by MyDelegate()
}

// يولِّد المترجم هذه الشيفرة بوجود الدالة
// 'provideDelegate'
class C {
    // استدعاء الدالة لإنشاء تعميم الخاصية الإضافية
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    val prop: Type
        get() = prop$delegate.getValue(this, this::prop)
}

حيث تؤثّر هذه الدالة على إنشاء الخاصية المساعدة (auxiliary property) فقط ولا تؤثِّر على الشيفرة المُولَّدة لأيِّ من getter أو setter.

مصادر