الدوال المباشرة (Inline Functions) في لغة Kotlin

من موسوعة حسوب

الدوال المباشرة (Inline Functions)

ينتُج عن استخدام الدوال من المرتبة الأعلى (higher-order functions) بعض التأثيرات السلبيّة أثناء التنفيذ (runtime)، إذ تُعدُّ كل دالة كائنًا (object) ضمن نطاقٍ مغلقٍ (closure) يشمل المتغيِّرات التي يمكن الوصول إليها في بُنية الدالة، كما ويتطلَّب ذلك تكلفةً إضافيّةً عند تخصيص جزءٍ من الذاكرة (لكلٍ من كائنات الدوال والأصناف [classes]) وعند الاستدعاءات الوهمية (virtual calls) أثناء التنفيذ.

وقد يُحدُّ من هذه المشاكل باللجوء إلى تعابير lambda المباشرة، إذ تُعدُّ الدالة lock()‎ مثالًا جيدًا لمثل هذه الحالات التي تُجعَل فيها lambda مباشرةً في موقع الاستدعاء، مثل:

lock(l) { foo() }

إذ بدلًا من إنشاء كائن الدالة للمعاملات وتوليد الاستدعاء يمكن أن يقوم المٌترجِم (compiler) بالشيفرة الآتية:

l.lock()
try {
    foo()
}
finally {
    l.unlock()
}

حيث يجب تحديد الدالة lock()‎ بالمُحدِّد inline كما يلي:

inline fun <T> lock(lock: Lock, body: () -> T): T {
    // ...
}

والذي سيؤثّر على كلٍّ من الدالة -ذاتها- وتعبير lambda المُمرَّر لها، حيث سيُضمَّن كل ذلك مباشرةً في موقع الاستدعاء، وقد تصبح الشيفرة أكثر طولًا، ولكن إن تمّ هذا بطريقةٍ سديدةٍ (بتجنُّب التضمين المباشر للدوال الضخمة) فسيُجدي ذلك في الأداء، وخاصة عند مواقع الاستدعاء في الحلقات (loops).

المُحدِّد noinline

قد نحتاج أحيانًا إلى التضمين المباشر "للبعض" (والبعض فقط) من تعابير lambda (المُمرَّرة إلى الدوال المباشرة)، يُستخدَم المُحدِّد noinline عندئذٍ لتحديد العناصر التي لا تخضع للتضمين المباشر، كما في الشيفرة:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
    // ...
}

إذ يُسمَح باستدعاء تعابير lambda المباشرة داخل الدوال المباشرة فقط أو بتمريرها كوسيطٍ مباشرٍ، أمّا العناصر المحُدَّدة بالكلمة المفتاحيّة noinline فيمكن أن تُعامَل بأيّة طريقة كانت، كتخزينها في الحقول (fields) أو تمريرها أو ... إلخ.

وتجدر الإشارة إلى أنّ المترجِم سيُعلِم بتحذيرٍ إن احتوت الدالة المباشرة على معاملاتٍ غير مباشرة أو معاملات نوعٍ ليست reified (كما ستُشرَح لاحقًا)، لأنّ ذلك لا يُجدي نفعًا، وبالإمكان تجاوز هذا التحذير -عند الحاجة المُلحَّة للتضمين المباشر- باستخدام التوصيف (annotation) الآتي: ‎@Suppress("NOTHING_TO_INLINE")‎.

أوامر العودة غير المحليّة (Non-local returns)

نستطيع في Kotlin استخدام الأمر return غير المقيّد (unqualified) للخروج من الدالة المُسمّاة (named function) أو الدالة المجهولة (anonymous function)، أمّا للعودة من تعبير lambda يجب استخدام التسمية (label)، ولا يُسمَح بأمر return مفردًا كما هو داخل التعبير، لأن تعبير lambda غير قادرٍ على العودة من الدالة المحيطة به (enclosing)، مثل:

fun foo() {
    ordinaryFunction {
        return // سينتج خطأ: لا يمكن العودة من الدالة هنا
    }
}

أمّا إن كانت الدالة التي يُمرَّر لها التعبير lambda مباشرةً فيمكن عندئذٍ جعل أمر العودة مباشرًا كذلك، وبالتالي تكون الشيفرة الآتية صحيحةً:

fun foo() {
    inlineFunction {
        return // صحيحة لأن التعبير lambda
               // من النوع المباشر
    }
}

وتُسمّى أوامر الرجوع هذه (والموجودة في تعبير lambda ولكنّها المسؤولة عن العودة من الدالة المحيطة به) بأوامر الرجوع غير المحليّة، وهذا ما تكون عليه عادةً بُنية الحلقات التي تحيط بها الدوال المباشرة، مثل:

fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if (it == 0) return true // العودة من hasZeros
    }
    return false
}

ويمكن لبعض الدوال المباشرة استدعاء تعابير lambda المُمرَّرة لها كمعاملاتٍ، ولا يتمّ ذلك من بُنية الدالة مباشرةً (directly) وإنمّا من أي سياقٍ تنفيذيّ آخر كالكائن المحلي (local object) أو دالةٍ متداخلةٍ (nested)، ولا يُسمَح حينها بالتحكم بالتدفق غير المحليّ في تعابير lamnda، إذ يجب أن يكون معامل lambda مُحدَّدًا بالكلمة المفتاحيّة crossinline كما في الشيفرة:

inline fun f(crossinline body: () -> Unit) {
    val f = object: Runnable {
        override fun run() = body()
    }
    // ...
}

ملاحظة: إنّ استخدام الأمرين break و continue في دوال lambda المباشرة في Kotlin غير ممكنٍ، وقد يُسمَح بذلك في الإصدارات القادمة.

المعاملات من النوع reified 

قد نحتاج أحيانًا للوصول إلى نوعٍ مُمرَّر كمعاملٍ (parameter)، مثل:

fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
    var p = parent
    while (p != null && !clazz.isInstance(p)) {
        p = p.parent
    }
    @Suppress("UNCHECKED_CAST")
    return p as T?
}

إذ سيجري عبور الشجرة حيث ستُستخدَم الانعكاسات (reflections) للتحقُّق من النوع في العقد، وسيكون موقع الاستدعاء بالشكل:

treeNode.findParentOfType(MyTreeNode::class.java)

وما نريده فعليًا هو تمرير النوع لهذه الدالة، أي استدعاؤها كما يلي:

treeNode.findParentOfType<MyTreeNode>()

للقيام بذلك تُستخدَم المعاملات من النوع reified ، وتصبح الشيفرة بالشكل:

inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}

إذ تُقيَّد معاملات النوع بالمُحدِّد reified مما يجعلها متاحةً داخل الدالة وكأنها صنف عاديّ. وبما أن الدالة مباشرةٌ فلا حاجة لأي انعكاس (reflection) هنا، ويُتاح استخدام المعاملات مثل ‎!is وas كذلك، كما ويمكن استدعاؤها -كما ذُكر سابقًا- بالصيغة: myTree.findParentOfType<MyTreeNodeType>()‎. وعلى الرغم من أنه لا حاجة للانعكاسات في كثيرٍ من الحالات، فما يزال استخدامها ممكنًا مع معاملات النوع reified كما يلي:

inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
    println(membersOf<StringBuilder>().joinToString("\n"))
}

ولا يمكن أن تحتوي الدوال العادية على معاملاتٍ من النوع reified ؛ لأنه لا يُسمَح للنوع الذي لا يملك تمثيلًا تنفيذيًا (كالمعامل الذي لا يكون reified أو النوع الزائف مثل Nothing) أن يكون من النوع reified.

الخاصّيّات المباشرة (بدءًا من الإصدار 1.1)

تُستخدَم الكلمة المفتاحيّة inline للوصول إلى الخاصّيّات التي ليس لها حقولٌ مساعدةٌ (backing fields)، وبالإمكان توصيف عمليات الوصول بشكلٍ منفصلٍ عن بعضهما كما يلي:

val foo: Foo
    inline get() = Foo()

var bar: Bar
    get() = ...
    inline set(v) { ... }

كما ويمكن توصيف الخاصية بمجملها، وهذا يجعل كلًا من get وset مباشرتين:

inline var bar: Bar
    get() = ...
    set(v) { ... }

إذ تُضمّن مباشرةً في موقع الاستدعاء كما لو أنها دالةٌ مباشرةٌ عاديّة.

التقييدات على الدوال المباشرة في الواجهات العامة (Public API)

عندما تكون الدالة المباشرة من النوع public أو protected وليست جزءًا من تصريحٍ من النوع private أو internal فإنها حينئذٍ تُعدُّ واجهةً عامةً للوحدة (module)، وبالتالي فمن الممكن استدعاؤها من أيّ وحدةٍ أخرى، كما ويمكن جعلها مباشرةً في مواقع الاستدعاء، لكنّ هذا سيفرض بعض المخاطر من ناحية عدم التوافق الثنائيّ (binary incompatibility) والذي يحدث نتيجةً لبعض التغييرات في الوحدة التي تصرِّح عن الدالة المباشرة، وذلك إن كان استدعاء الوحدة لا يُترجَم ثانيةً بعد ذلك التغيير.

وللحد من هذه المخاطر لا يُسمح للدوال المباشرة في واجهات API العامّة باستخدام تصريحات الواجهات غير العامّة (non-public-API declarations)، أي لا يُسمح لها بالتصريحات من النوعين private و internal في بُنيتها.

وتُتاح إضافة التوصيف (annotation) بالصيغة ‎@PublishedApi إلى التصريح من النوع internal ، ممّا سيسمح باستخدامه في الدوال المباشرة في الواجهات العامّة، وعند تحديد الدالة المباشرة من النوع internal (عبر التوصيف ‎@PublishedApi) فسيُتحقَّق من بُنيتها وكأنها عامّة (public).

مصادر