الانعكاس (Reflection) في لغة Kotlin
الانعكاس هو مجموعةٌ من مميّزات اللغة والمكتبات التي تسمح بمراقبة بُنية البرنامج أثناء التنفيذ، إذ تولي لغة Kotlin أهميةً كبرى لكلٍّ من الدوال (functions) والخاصّيّات (properties) وتستمر بتتبُّعها (مثل معرفة اسم أو نوع الخاصّيّة والدالة أثناء التنفيذ) وهذا يرتبط ارتباطًا وثيقًا مع استخدام النمط الوظائفيّ (functional) أو التفاعليّ (reactive).
ملاحظة: إن الجزء التنفيذيّ (runtime component) المطلوب في منصّة العمل Java لاستخدام ميزات الانعكاس يُوزَّع كملف .jar
مستقلٍّ (kotlin-reflect.jar
) وذلك بهدف تخفيض حجم التخزين المطلوب لمكتبة التنفيذ (runtime library) للتطبيقات غير المعتمِدة على الانعكاسات، ولدى الحاجة إليها فتجب إضافة الملف .jar
إلى مسار الصنف (classpath) في المشروع.
مرجعيّات الأصناف (Class References)
إن أهمّ ميزةٍ انعكاسيّةٍ هي الحصول على مرجعيّة الصنف في Kotlin أثناء التنفيذ، حيث تُستخدَم صيغة قيمة الصنف الحرفيّة (class literal) للحصول على المرجعية الستاتيكيّة (الساكنة) للصنف كما في الشيفرة:
val c = MyClass::class
حيث تكون قيمة المرجعيّة من النوع KClass
، ويختلف الأمر في أصناف Java إذ إنّه للحصول على مرجعيةّ الصنف تُستخدَم الخاصّيّة .java
على كائنٍ (instance) من الصنف KClass
.
مرجعيّات الأصناف المقيَّدة (Bound Class References) (بدءًا من الإصدار 1.1)
بالإمكان الحصول على مرجعيّة الصنف لكائنٍ (object) ما عبر نفس الصيغة السابقة (وهي ::class
) ولكن بجعل هذا الكائن كمستقبِلٍ (receiver) لها، مثل:
val widget: Widget = ...
assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" }
ستساعد الشيفرة السابقة في الحصول على مرجعيّةٍ لنفس صنف الكائن وليكن مثلًا GoodWidget
أو BadWidget
على الرغم من كون نوع التعبير المستقبِل Widget
.
مرجعيّات الدوال (Function References)
لتكن الدالة المُصرَّحة بالشكل:
fun isOdd(x: Int) = x % 2 != 0
إنّ من السهل استدعاؤها مباشرةً بالصيغة isOdd(5)
كما ويمكن تمريرها كقيمةٍ (لدالة أخرى مثلًا)، حيث سيُستخدَم المعامل ::
بالشكل:
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // [1, 3] ستظهر النتيجة
إذ تُعدُّ ::isOdd
قيمةً من نوع الدالة (Int) -> Boolean
.
كما يُتاح استخدام المعامل ::
في الدوال زائدة التحميل (overloaded functions) عندما يكون النوع المُتوقَّع معروفًا من السياق العامّ، مثل:
fun isOdd(x: Int) = x % 2 != 0
fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove"
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // مرجعية للدالة isOdd(x: Int)
أو قد يُزوَّد السياق المطلوب عبر تخزين مرجعيّة التابع (method) بمتحوّلٍ من نوعٍ مُحدِّد بشكل صريحٍ (explicitly) كما في الشيفرة:
val predicate: (String) -> Boolean = ::isOdd // مرجعية للدالة isOdd(x: String)
وعند الحاجة لاستخدام عنصرٍ (member) من الصنف أو دالةٍ إضافيّةٍ فيجب تقييده، فسينتج مثلًا عن String::toCharArray
دالةٌ إضافيّةٌ للنوع String
وهي: String.() -> CharArray
.
مثال: تشكيل الدالة (Function Composition)
لتكن الدالة الآتية:
fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}
والتي تُعيد تركيبًا من دالتين مُمرَّرتين لها بالشكل compose(f, g) = f(g(*))
، وبالتالي يمكن تطبيقها عند المرجعيات القابلة للاستدعاء كما في الشكل:
fun length(s: String) = s.length
val oddLength = compose(::isOdd, ::length)
val strings = listOf("a", "ab", "abc")
println(strings.filter(oddLength)) // "[a, abc]" ستظهر النتيجة
مرجعيّات الخاصّيّات (Property References)
يُسمح باستخدام المعامل ::
أيضًا للوصول إلى الخاصّيّات بالشكل:
val x = 1
fun main(args: Array<String>) {
println(::x.get()) // "1" ستُطبَع النتيجة
println(::x.name) // "x" ستُطبَع النتيجة
}
إذ يُحسَب التعبير ::x
لكائن الخاصّيّة (property object
) من النوع KProperty<Int>
، وهذا ما يُتيح الحصول على قيمتها عبر get()
أو استعادة اسم الخاصّيّة من خلال خاصّيّة الاسم name
، أمّا في حالة الخاصّيّة المتغيّرة (مثل: var y = 1
) فإن التعبير ::y
سيُعيد قيمةً من النوع KMutableProperty<Int>
والذي يحتوي على set()
كما في الشيفرة:
var y = 1
fun main(args: Array<String>) {
::y.set(2)
println(y) // "2" ستطبع القيمة
}
وبالإمكان استخدام مرجعيّة الخاصّيّة عند توقَّع الدالة الخالية من المتحولات مثل:
val strs = listOf("a", "bc", "def")
println(strs.map(String::length)) // [1, 2, 3] ستظهر النتيجة
ويُلجأ عند الوصول إلى الخاصّيّة التي تُعدُّ عنصرًا في الصنف إلى تقييدها (qualify) مثل:
class A(val p: Int)
fun main(args: Array<String>) {
val prop = A::p
println(prop.get(A(1))) // "1" ستظهر القيمة
}
وتصبح الشيفرة بالنسبة للخاصّيّة الإضافيّة (extension property) بالشكل:
val String.lastChar: Char
get() = this[length - 1]
fun main(args: Array<String>) {
println(String::lastChar.get("abc")) // "c" ستظهر النتيجة
}
قابليّة التبديل (interoperability) مع انعكاس Java
تحتوي المكتبة القياسيّة في منصّة Java على الإضافات (extensions) للأصناف الانعكاسيّة (reflection classes) والتي تُزوِّد بعمليات الاقتران (mapping) من وإلى كائنات Java الانعكاسية (راجع الحزمة kotlin.reflect.jvm
)، فللبحث مثلًا عن الحقل المساعد (backing field) أو تابع Java للقيام بمهمة getter للخاصّيّة في Kotlin فعندها ستكون الشيفرة:
import kotlin.reflect.jvm.*
class A(val p: Int)
fun main(args: Array<String>) {
println(A::p.javaGetter) // ستظهر النتيجة "public final int A.getP()"
println(A::p.javaField) // ستظهر النتيجة "private final int A.p"
}
وللحصول على صنف Kotlin بما يتوافق مع صنف Java تُستخدَم الخاصّية الإضافيّة .kotlin
كما في الشيفرة:
fun getKClass(o: Any): KClass<Any> = o.javaClass.kotlin
مرجعيّات الباني (Constructor References)
يُتاح إنشاء مرجعيّةٍ للباني كما هو الحال في مرجعيّات التوابع (methods) والخاصّيّات (properties)، وذلك عند توقُّع كائنٍ من نوع الدالة (function) والذي يأخذ نفس المتحولات كما الباني ويعيد كائنًا بالنوع المناسب، وتكون الإشارة إلى مرجعيّة الباني عبر المعامل ::
ملحقًا به اسم الصنف (class).
لتكن الدالة الآتية التي تتوقَّع متحولَّ دالةٍ خاليةٍ من المتحولات وتعيد النوع Foo
:
class Foo
fun function(factory: () -> Foo) {
val x: Foo = factory()
}
تُستدعَى هذه الدالة باستخدام ::Foo
(وهو الباني الخالي من المتحولات للصنف Foo
) كما يلي:
function(::Foo)
مرجعيات الخاصّيّة والدالة المقيَّدة (بدءًا من الإصدار 1.1)
من الممكن الإشارة إلى تابع الكائن (instance method) لكائنٍ ما وفق الشيفرة:
val numberRegex = "\\d+".toRegex()
println(numberRegex.matches("29")) // "true" ستظهر النتيجة
val isNumber = numberRegex::matches
println(isNumber("29")) // "true" ستظهر النتيجة
فبدلًا من استدعاء التابع matches
مباشرةً تُخزَّن مرجعيةٌ له، إذ تكون هذه المرجعيّة مقيَّدة بالمستقبِل (receiver)، ويمكن استدعاؤها مباشرةً (كما في المثال السابق) أو أن تُستخدم عندما يُتوقَّع أي تعبيرٍ لنوع الدالة، مثل:
val strings = listOf("abc", "124", "a70")
println(strings.filter(numberRegex::matches)) // "[124]" ستظهر النتيجة
وبالمقارنة بين الأنواع المقيّدة مع مثيلتها غير المقيَّدة يكون للمرجعيّات المقيَّدة -والقابلة للاستدعاء (callable)- مستقبِلٌ ملحقٌ بها، وبالتالي فإن نوع المستقبِل لا يعد متحولًا، مثل الشيفرة:
val isNumber: (CharSequence) -> Boolean = numberRegex::matches
val matches: (Regex, CharSequence) -> Boolean = Regex::matches
كما ويمكن تقييد مرجعيّة الخاصّيّة أيضًا، مثل:
val prop = "abc"::length
println(prop.get()) // "3" ستطبع القيمة
ولم يعد من الضروريّ بدءًا من الإصدار Kotlin 1.2 التحديد الصريح للمعامل this
كمستقبِلٍ، وبذلك أصبحت الصيغة this::foo
تكافئ ::foo
.