الخاصّيّات المُعمَّمة (Delegated Properties) في لغة Kotlin
استخدام الخاصّيّات المُعمَّمة
تستطيع في لغة 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.