التحقق من الأنواع (Type Check) والتحويل بينها (Casting) في لغة Kotlin

من موسوعة حسوب
اذهب إلى: تصفح، ابحث

المعاملين is و ‎!is

تدعم لغة Kotlin ميّزة التحقُّق من توافق الكائن مع أحد الأنواع أثناء التنفيذ، وذلك بالاعتماد على المُعامِل is أو صيغته المنفيّة ‎!is كما في الشيفرة:
if (obj is String) {
    print(obj.length)
}

if (obj !is String) { // !(obj is String) مكافئ للصيغة
    print("Not a String")
}
else {
    print(obj.length)
}

التحويلات الذكية (Smart Casts)

لا حاجة في كثيرٍ من الأحيان لجعل التحويل صريحًا (explicit) في لغة Kotlin لأنّ المترجم (compiler) يتتبَّع عمليات التحقُّق (عبر المعامل is) والتحويل الصريح للقيم الثابتة (immutable) ويُحوِّلها تلقائيًا وبشكلٍ آمنٍ عندما تدعو الحاجة لذلك، كما في الشيفرة:
fun demo(x: Any) {
    if (x is String) {
        print(x.length) // x سيُحوّل المعامل
                        // String تلقائيًا إلى النوع
    }
}
حيث يُعدُّ التحويل "آمنًا" إن كان التحقُّق المنفيّ يؤدي إلى أمر الرجوع return مثل:
    if (x !is String) return
    print(x.length) // x سيُحوّل المعامل
                    // String تلقائيًا إلى النوع
أو في الجانب الأيمن من المعامِلين && أو || كما في الشيفرة:
    // سيُحوّل المتغير تلقائيًا إلى نوع السلسلة النصيّة 
    // لوقوعه على الجانب الأيمن من المعامل || في الشرط 
    if (x !is String || x.length == 0) return

    // سيُحوّل المتغير تلقائيًا إلى نوع السلسلة النصيّة 
    // لوقوعه على الجانب الأيمن من المعامل && في الشرط 
    if (x is String && x.length > 0) {
        print(x.length) // x is automatically cast to String
    }
وبالإمكان استخدام مثل هذه التحويلات الذكيّة في تعابير when وحلقات while أيضًا كما في الشيفرة:
when (x) {
    is Int -> print(x + 1)
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}
ولا تُجرَى التحويلات الذكيّة عندما يكون المترجِم (compiler) غير قادرٍ على ضمان عدم تغيُّر المتغيِّر بين عملية التحقُّق والاستخدام، وبتفصيلٍ أكثر؛ تُجرَى التحويلات الذكيّة في الحالات الآتية:
  • المتغيِّرات المحليّة (local) من النوع val: دائمًا باستثناء الخاصّيّات المُعمَّمة المحليّة (local delegated properties).
  • الخاصّيّات (properties) من النوع val: فقط إن كانت الخاصّيّات خاصةً (private) أو داخليةً (internal) أو أُجري التحقُّق منها في نفس الوحدة (module) التي تحتوي على تصريح الخاصّيّة، ولا تتمّ التحويلات في حالة الخاصّيّات المفتوحة (open) أو تلك التي يكون فيها getter مُخصَّص.
  • المتغيِّرات المحليّة من النوع var: فقط إن لم يتغيّر المتغيِّر ما بين التحقُّق والاستخدام ولم يخضغ لتعبير lambda يحوِّل من قيمته أو لم يكن خاصّيّةً مُعمَّمةً محليّةً.
  • الخاصّيّات من النوع var: أبدًا، وذلك لأنّ المتغيِّر قد يتغيّر في أيِّ لحظةٍ ممكنةٍ وعبر أيّ شيفرة أخرى.

معامل التحويل غير الآمن

سينتُج عن استخدام معامل التحويل استثناءٌ (exception) إن لم يكن التحويل متاحًا، وهذا ما ندعوه بالتحويل غير الآمن (unsafe) ويُعتمَد في Kotlin على المعامل as بالصيغة الداخليّة (infix) كما في الشيفرة:
val x: String = y as String
إذ لا يُمكن تحويل القيمة الفارغة null إلى النوع String لأنّه ليس نوعًا nullable، وبالتالي فإن كانت قيمة المتغيِّر y هي null سينتُج استثناء. ولتحقيق التماثل مع صيغة لغة Java يجب أن يكون النوع nullable في الجانب الأيمن من التحويل كما في الشيفرة:
val x: String? = y as String?

معامل التحويل الآمن (Nullable)

يُستخدَم معامل التحويل الآمن as?‎ (والذي يعيد القيمة null عند حدوث أيِّ خللٍ) لمنع حدوث الاستثناء (exception) السابق، مثل:
val x: String? = y as? String
وعلى الرغم من أنّ الجانب الأيمن من المعامِل as?‎ هو من النوع String (الذي لا يقبل null) فقد تكون نتيجة التحويل الإجماليّة null (أي أنّها nullable).

إزالة الأنواع (Type Erasure) والتحقُّق من الأنواع المُعمَّمة (Generic Type)

تضمن لغة Kotlin أمان الأنواع (type-safety) -بما فيها الأنواع المُعمَّمة (generics)- أثناء الترجمة (compilation) فقط، أما أثناء التنفيذ (runtime) فقد لا تحتوي بعض الكائنات من الأنواع المُعمَّمة على معلوماتٍ عن وسائط أنواعها الفعليّة، فيُزال النوع List<Foo>‎ مثلًا ليشمله النوع List<*>‎، إذ لا توجد طريقةٌ للتحقُّق من انتماء الكائن إلى نوعٍ مُعمَّمٍ (بوسائط الأنواع) أثناء التنفيذ.

وبالتالي فإن المُترجِم (compiler) يمنع عمليات التحقُّق بالمعامل is أثناء التنفيذ بسبب إزالة الأنواع (type erasure) مثل: ints is List<Int>‎ أو list is T ، ولكن من الممكن التحقُّق من الكائنات المخالفة للأنواع ذات الإسقاط الواسع (star-projected type) كما في الشيفرة:
if (something is List<*>) {
    something.forEach { println(it) } // `Any?` ستُجعَل العناصر من النوع
}
وبشكلٍ مماثلٍ؛ وبما أنه تمّ التحقُّق الستاتيكيّ لوسائط الأنواع (type arguments) للكائن (instance) أثناء الترجمة فيمكن كذلك التحقُّق بالمعامل is أو القيام بأيّ تحويلٍ يحتوي على الجزء غير المُعمَّم (non-generic part) من هذا النوع، حيث ستُحذف الأقواس <> كما في الشيفرة:
fun handleStrings(list: List<String>) {
    if (list is ArrayList) {
        // سيُحوَّل الكائن list
        // إلى النوع `ArrayList<String>`
    }
}
ويُسمَح باستخدام نفس صيغة التعامل مع وسائط النوع المحذوفة في حالة التحويلات التي لا تأخذ وسائط النوع بالحسبان، مثل: list as ArrayList. وفي حالة الدوال المباشرة (inline functions) بمعاملات نوعٍ reified (والمُضمَّنة [inlined] في كلّ موقع استدعاءٍ للدالة) فيُتاح التحقُّق من معاملات النوع بالشكل: arg is T ، أما إن كان arg كائنًا (instance) من النوع المُعمَّم ذاته فستُزال كذلك وسائط النوع فيه، مثل:
inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
    if (first !is A || second !is B) return null
    return first as A to second as B
}

val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)

val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()
val stringToList = somePair.asPairOf<String, List<*>>()
val stringToStringList = somePair.asPairOf<String, List<String>>() // Breaks type safety!

التحويلات غير المتحقَّق منها (Unchecked Casts)

إنّ مفهوم إزالة الأنواع (type erasure) في Kotlin يمنع -كما ذكرنا آنفًا- عمليات التحقُّق من وسائط النوع الفعليّ لكائنات الأنواع المُعمَّمة (generic type instances) أثناء التنفيذ (runtime)، وقد لا ترتبط الأنواع المُعمَّمة فيما بينها بما يكفي المترجمَ لضمان أمان الأنواع، وعلى الرغم من ذلك تشتمل (imply) بعض البرامج عالية المستوى (high-level) على أمان الأنواع، مثل:
fun readDictionary(file: File): Map<String, *> = file.inputStream().use { 
    TODO("Read a mapping of strings to arbitrary elements.")
}
// عملية حفظ map
// بقيم صحيحة في ذلك الملف
val intsFile = File("ints.dictionary")

// تحذير: عملية تحويل غير مُتحقَّق منها 
// `Map<String, *>` to `Map<String, Int>`
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>
إذ يُصدِر المترجم تحذيرًا عند التحويل في السطر الأخير من الشيفرة حيث لا يمكن التحقُّق منه كليَّا أثناء التنفيذ وليس من المؤكَّد أن تكون القيم في كائن map من النوع Int.

ولتجنب ذلك النوع من التحويلات تُعاد هيكلة البرنامج؛ فبالإمكان إضافة الواجهتين: DictionaryReader<T>‎ و DictionaryWriter<T>‎ في البرنامج السابق واللتان تحتويان على تعريف استخدامٍ (implementation) آمنٍ لأنواع مختلفةٍ، ومن الممكن أيضًا إضافة تجريدات (abstractions) لنقل التحويل غير المتحقَّق منه (unchecked cast) من الشيفرة المستدعِية (calling cod) إلى تعريف الاستخدام (implementation)، ومن المفيد كذلك استخدام التغيُّر المُعمَّم (generic variance).

أمّا في حالة الدوال المُعمَّمة (generic functions) فإنّ استخدام معاملات الأنواع من النوع reified يجعل التحويل مثل: arg as T محقَّقًا (checked) ما لم يكن لنوع arg متحولات النوع الخاصّة به والتي ستُزال.

وللتجاوز عن تحذير التحويل غير المتحقَّق منه يضاف التوصيف (annontation) بالصيغة ‎@Suppress("UNCHECKED_CAST")‎ للتعليمة أو التصريح الذي يحدث فيه ذلك التحذير، كما في الشيفرة:
inline fun <reified T> List<*>.asListOfType(): List<T>? =
    if (all { it is T })
        @Suppress("UNCHECKED_CAST")
        this as List<T> else
        null
إذ تحتفظ أنواع المصفوفات (array types) في بيئة JVM (مثل Array<Foo>‎) بمعلوماتٍ عن النوع المُزال (erased) لعناصرها، ويُتحقَّق جزئيًا من عملية التحويل إلى نوع المصفوفة حيث ستبقى قابلية احتواء القيمة الفارغة (nullability) ووسائط النوع الفعلي للعناصر مُزالةً (erasaed)، فسينجح مثلًا التحويل foo as Array<List<String>?>‎ إن كانت foo  مصفوفةً تحمل أيًا من List<*>‎ سواءً أكانت nullable أم لا.

مصادر