الخاصّيّات المُعمَّمة (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
قبل تنفيذ عملية الإسناد للخاصّيّة.