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

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

التصريح عن التوصيف (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
}

مصادر