الانعكاس (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.

مصادر