الدوال السطريّة (Inline Functions) في لغة Kotlin

من موسوعة حسوب
مراجعة 16:57، 24 مارس 2018 بواسطة Nourtam (نقاش | مساهمات) (أنشأ الصفحة ب'<noinclude>{{DISPLAYTITLE:الدوال السطريّة (Inline Functions) في لغة Kotlin}}</noinclude> == الدوال السطريّة (Inline Functions) == يفر...')
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)

الدوال السطريّة (Inline Functions)

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

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

lock(l) { foo() }

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

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

وللقيام بذلك يجب تحديد الدالة الدالة lock()‎ بالمحدد inline كما يلي:

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

والذي يؤثر على كلٍ من الدالة ذاتها وتعبير lambda الممرر لها، حيث سيتم تضمين كل ذلك سطريًا في موقع الاستدعاء.

وقد يجعل ذلك الشيفرة أطول ولكن إن كان القيام بذلك منطقي (كتجنب الدوال السطرية الطويلة) فسيصب ذلك لصالح الأداء وخاصة عند مواقع الاستدعاء في الحلقات.

المحدد noinline

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

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

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

ويجدر الإشارة إلى أن المترجم سيعلم بتحذيرٍ إن احتوت الدالة السطرية على متحولات غير سطرية أو متحولات من النوع reified، لأن مثل هذا التضمين السطري لن يكون نافعًا، ويمكن تجاوز هذا التحذير عند التأكد من الحاجة الملحة له باستخدام التوصيف ‎@Suppress("NOTHING_TO_INLINE")‎.

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

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

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 الممررة لها كمتحولات، ليس مباشرةً من بنية الدالة وإنما من أي سياق تنفيذيّ آخر كالكائن المحلي أو دالة متداخلة، وبهذه الحالات لا يسمح بالتحكم بالتدفق غير المحلي في تعابير lamnda، إذ يجب أن تكون محددة بالكلمة المفتاحية crossinline كما في الشيفرة:

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

لا يسمح باستخدام الأمرين break و continue في دوال lambda السطرية في Kotlin وقد يسمح بذلك في الإصدارات القادمة.

المتحولات من النوع Reified 

قد نحتاج أحيانًا للوصول إلى نوعٍ ممرر كمتحول، مثل:

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?
}

حيث سيتم المرور بعقد الشجرة وتستخدم الانعكاسات هنا للتحقق من النوع في تلك العقد وسيكون موقع الاستدعاء بالشكل:

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 مما يجعلها متاحة داخل الدالة وكأنها صنف عاديّ. وبما أن الدالة سطرية فلا حاجة لأي انعكاس هنا، ويتاح استخدام المعاملات مثل ‎!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 إذ لا يمكن للنوع الذي لا يملك تمثيلًا تنفيذيًا (كمتحول من النوع non-reified أو النوع الزائف مثل Nothing) أن يستخدم كمتحولٍ من النوع reified.

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

يمكن استخدام الكلمة المفتاحية inline للوصول إلى الخاصيات التي ليس لها حقول مساعدة، ويمكن توصيف الوصول إلى الخاصية بشكل منفردٍ كما يلي:

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 فإنها حينئذٍ تعد واجهة عامة للوحدة، وبالتالي يمكن استدعاؤها من أي وحدة أخرى كما ويمكن جعلها سطرية في مواقع الاستدعاء.

وهذا سيفرض بعض المخاطر من ناحية توافقية التبديل الثنائية التي تحدث نتيجة بعض التغييرات في الوحدة التي تصرح عن الدالة السطرية إن كان استدعاء الوحدة لا يترجم ثانية بعد ذلك التغيير.

وللحد من هذه المخاطر، لا يسمح باستخدام تصريحات الواجهات غير العامة من قِبل الدوال السطرية في واجهات API العامّة أي لا يسمح لها بالتصريحات من النوع private أو internal في بنيتها.

ويسمح بإضافة التوصيف ‎@PublishedApi للتصريح من النوع internal مما سيسمح باستخدامه في الدوال السطرية في الواجهات العامّة، وعند تحديد الدالة السطرية من النوع internal بالتوصيف ‎@PublishedApi فسيتمّ التحقق من بنية الدالة حينها وكأنها عامة.

مصادر