التوصيفات (Annotations) في لغة Kotlin

من موسوعة حسوب
مراجعة 17:47، 4 يوليو 2018 بواسطة Nourtam (نقاش | مساهمات) (تعديل مصطلح متحول)

التصريح عن التوصيف (Annotation Declaration)

تُعدُّ التوصيفات إحدى الوسائل لإضافة بياناتٍ توصيفيّةٍ (metadata) إلى الشيفرة، وللتصريح عن التوصيف يُضاف المُحدِّد annotation قبل اسم الصنف، مثل:

annotation class Fancy

وقد تُحدَّد بعض خواصّ التوصيفات (annotation attributes) باستخدام التوصيفات الآتية (meta-annotations) لتوصيفات الصنف:

  • ‎@Target لتحديد نوع العناصر التي يمكن توصيفها مثل الأصناف (classes) والدوال (functions) والخاصّيّات (properties) والتعابير (expressions) و... إلخ.
  • @Retention لتحديد فيما إن كان التوصيف مُخزَّنًا في ملفات الأصناف المُترجَمة، أو مرئيًا عبر انعكاسٍ (reflection) أثناء التنفيذ (runtime) (وكلاهما محقُّق بالحالة الافتراضية)
  • @Repeatable للسماح باستخدام نفس التوصيف على كائن واحدٍ لعدّة مرات
  • @MustBeDocumented  لتحديد التوصيف كجزءٍ من الواجهة العامّة (public API) يجب تضمينه في الصنف (class) أو ترويسة التابع (method signature) الموجودَين في التوثيق المُولَّد للواجهة API

مثال:

@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
        AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy

الاستخدام (Usage)

ليكن الصنف Foo الآتي:

@Fancy class Foo {
    @Fancy fun baz(@Fancy foo: Int): Int {
        return (@Fancy 1)
    }
}

لتوصيف الباني الأساسيّ (primary constructor) للصنف تجب إضافة الكلمة المفتاحيّة constructor إلى تصريحه مسبوقةً بالتوصيف كما في الشيفرة:

class Foo @Inject constructor(dependency: MyDependency) {
    // ...
}

كما ويمكن توصيف الوصول (accessing) إلى الخاصّيّات (get أو set) كما في الشيفرة:

class Foo {
    var x: MyDependency? = null
        @Inject set
}

البواني (Constructors)

وقد يكون للتوصيفات بوانٍ تقبل المعاملات (parameters)، مثل:

annotation class Special(val why: String)

@Special("example") class Foo {}

حيث يُسمَح بأنواع المعاملات الآتية:

  • الأنواع الموافقة للأنواع الأساسيّة (primitive) في لغة Java (مثل int و Long و... وإلخ.)
  • السلاسل النصيّة (strings)
  • الأصناف (classes) (بالصيغة Foo::class)
  • الثوابت المتعدِّدة (enums)
  • التوصيفات الأخرى
  • مصفوفة (array) من أيّ من الأنواع السابقة

ولا يُسمَح أن تكون هذه الأنواع nullable لأن JVM لا تدعم تخزين القيمة null في خاصّة التوصيف (annotation attribute).

وعند استخدام التوصيف كمعاملٍ (parameter) لتوصيفٍ آخر فلا يُسبَق اسمه بالرمز @ مثل:

annotation class ReplaceWith(val expression: String)

annotation class Deprecated(
        val message: String,
        val replaceWith: ReplaceWith = ReplaceWith(""))

@Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other"))

وعند تحديد الصنف (class) كوسيطٍ للتوصيف فيُستخدَم صنف Kotlin والمُسمّى KClass ، والذي يُحوِّله المُترجِم إلى صنف Java تلقائيًا بحيث تصبح شيفرة Java قادرةً على رؤية التوصيفات والوسائط بشكلٍ طبيعيّ، مثل:

import kotlin.reflect.KClass

annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any>)

@Ann(String::class, Int::class) class MyClass

تعابير Lambdas

تُضاف التوصيفات لتعابير lambdas عبر تطبيقها على التابع inoke()‎ الذي من خلاله ستُولَّد بُنية lambda، وهذا يُفيد في بيئات العمل (frameworks ) مثل Quasar التي تستخدِم التوصيفات للتحكم بالتزامن (concurrency)، مثل:

annotation class Suspendable

val f = @Suspendable { Fiber.sleep(10) }

أهداف مواقع استخدام التوصيفات (Annotation Use-site Targets)

عند توصيف إحدى الخاصّيّات (property) أو معاملات الباني الأساسي (primary constructor parameter) فهناك الكثير من عناصر Java المُولَّدة من عناصر Kotlin الموافقة لها، وبالتالي تتعدّد المواقع المُمكِنة لاستخدام التوصيف في شيفرة Java bytecode المُولَّدة، ولتحديد كيفيّة توليد التوصيف بشكلٍ دقيقٍ، تُستخدَم الصيغة الآتية:

class Example(@field:Ann val foo,    // توصيف حقل Java
              @get:Ann val bar,      // توصيف الوصول عبر getter
              @param:Ann val quux)   // توصيف باني أساسي

وبالإمكان استخدام نفس الصيغة السابقة لتوصيف كامل الملفّ، إذ يُوضَع التوصيف بالهدف file في المستوى الأعلى (top-level) من الملف قبل مُوجِّه الحزمة (package directive) أو قبل أيٍّ من عمليات الاستيراد (import) إن كان الملف واقعًا في الحزمة الافتراضية، مثل:

@file:JvmName("Foo")

package org.jetbrains.demo

وعند وجود أكثر من توصيفٍ بنفس الهدف تُضاف الأقواس [] بعد الهدف مباشرةً ويُوضع كامل التوصيف داخلها وذلك لمنع التكرار ، مثل:

class Example {
     @set:[Inject VisibleForTesting]
     var collaborator: Collaborator
}

تدعم لغة Kotlin القائمة الآتية من الأهداف:

  • file
  • property (توصيف هذا الهدف غير مرئيٍّ بالنسبة للغة Java)
  • field
  • get (الحصول على الخاصّيّة عبر getter)
  • set (ضبط الخاصّيّة عبر setter)
  • receiver (معامل المستقبِل للدالة الإضافيّة [extension function] أو الخاصّيّة)
  • param (معامل الباني [constructor])
  • setparam (معامل ضبط الخاصّيّة عبر setter)
  • delegate (حقل تخزين نسخة التعميم (delegate instance) من الخاصّيّات المُعمَّمة)

ولتوصيف معامل المستقبِل (receiver parameter) في الدالة الإضافيّة (extension function) تُستخدَم الصيغة الآتية:

fun @receiver:Fancy String.myExtension() { }

وإن لم يُحدَّد هدف موقع الاستخدام (use-site target) فسيصبح الهدف بحسب توصيف ‎@Target من التوصيف المُستخدَم، أمّا إن كان هناك أكثرُ من هدفٍ محتملٍ، فسيتم اعتماد أولّ عنصرٍ ممكنٍ من هذه القائمة:

  • aram
  • property
  • field

التوصيفات في لغة Java

تتوافق التوصيفات في لغة Java بشكل تامّ مع لغة Kotlin، مثل:

import org.junit.Test
import org.junit.Assert.*
import org.junit.Rule
import org.junit.rules.*

class Tests {
    // تطبيق التوصيف @Rule
    // على الوصول للخاصية عبر getter
    @get:Rule val tempFolder = TemporaryFolder()

    @Test fun simple() {
        val f = tempFolder.newFile()
        assertEquals(42, getTheAnswer())
    }
}

إذ لا يمكن استخدام الصيغة الاعتياديّة لاستدعاء الدالة لتمرير الوسائط (arguments passing) لأن ترتيب الوسائط في التوصيف المكتوب بلغة Java غير معرَّف، بل تُستخدَم بدلًا عن ذلك صيغة الوسائط المُسمّاة كما في الشيفرة:

// Java شيفرة
public @interface Ann {
    int intValue();
    String stringValue();
}

وفي لغة Kotlin بالشكل:

// Kotlin شيفرة
@Ann(intValue = 1, stringValue = "abc") class C

وكما هو الحال في لغة Java فإن المعامل value هو حالةٌ خاصةٌ لأن قيمته تُحدَّد بدون استخدام اسمٍ صريحٍ (explicit)، كما في الشيفرة:

// Java شيفرة
public @interface AnnWithValue {
    String value();
}

وفي لغة Kotlin:

// Kotlin شيفرة
@AnnWithValue("abc") class C

استخدام المصفوفات كمعاملات للتوصيف

إن كان الوسيطvalue في لغة Java مصفوفةً فسيصبح معاملًا من النوع vararg في لغة Kotlin، مثل:

// Java شيفرة
public @interface AnnWithArrayValue {
    String[] value();
}

وفي لغة Kotlin تصبح الشيفرة:

// Kotlin شيفرة
@AnnWithArrayValue("abc", "foo", "bar") class C

أمّا إن كانت الوسائط الأخرى على شكل مصفوفاتٍ فيجب استخدام صيغة القيم المباشرة (literals) للمصفوفة (بدءًا من الإصدار Kotlin 1.2 ) أو استخدام arrayOf(...)‎، مثل:

// Java شيفرة
public @interface AnnWithArrayMethod {
    String[] names();
}

وتصبح في لغة Kotlin:

// Kotlin 1.2 بدءًا من الإصدار
@AnnWithArrayMethod(names = ["abc", "foo", "bar"]) 
class C

// الإصدارات الأقدم
@AnnWithArrayMethod(names = arrayOf("abc", "foo", "bar")) 
class D

الوصول إلى الخاصّيّات (Properties) عبر نسخة التوصيف (Annotation Instance)

تظهر قيم نسخة التوصيف (annotation instance) وكأنها خاصّيّات في شيفرة Kotlin كما يلي:

// Java شيفرة
public @interface Ann {
    int value();
}

وتصبح في لغة Kotlin:

// Kotlin شيفرة
fun foo(ann: Ann) {
    val i = ann.value
}

مصادر