الدالة المضمنة — آلية في Kotlin يتم فيها استبدال جسم الدالة مباشرة في كل موقع استدعاء في وقت الترجمة. هذا يزيل الحمل الزائد الناتج عن إنشاء فئات مجهولة وكائنات لمعاملات الامدا. وفقًا لـ Kotlin Documentation, 2025، فإن الكلمة المفتاحية inline فعالة بشكل خاص للدوال عالية الترتيب، حيث كل لامدا بدون تضمين تنشئ كائن FunctionN منفصل، مما يثقل كاهل جامع القمامة.
النقاط الرئيسية
الدالة المضمنة هي دالة موسومة بالكلمة المفتاحية inline. لا ينشئ مترجم Kotlin كود بايت منفصل مع استدعاء لها — بل ينسخ جسم الدالة مباشرة في كل موقع استدعاء. الهدف الرئيسي هو تحسين الدوال عالية الترتيب التي تقبل تعبيرات لامدا، حيث أن كل لامدا في الوضع الطبيعي تنشئ كائن فئة Function مجهول.
وفقًا لـ JetBrains Tech Blog (2024)، يمكن أن يؤدي استخدام الدوال المضمنة في Kotlin إلى تقليل عدد الكائنات المنشأة بنسبة 40–60% في الدوال التي تستخدم الامدا بشكل مكثف. في الحلقات والعمليات عالية التحميل (الترتيب، تصفية المجموعات) يوفر هذا تحسينًا ملموسًا في الأداء.
بدون inline، يتم ترجمة كل لامدا إلى فئة مجهولة (أو مثيل لواجهة وظيفية مركبة). بالنسبة للامدا التي تلتقط المتغيرات، يتم إنشاء كائنات غلاف إضافية. تزيل الدوال المضمنة كل هذه الكائنات في وقت الترجمة، وتستبدلها بكود مباشر يصل إلى المتغيرات المحلية بدون أغلفة.
استخدم inline فقط للدوال ذات معاملات لامدا — مترجم Kotlin نفسه يحذر إذا كان inline لا يوفر فائدة.
ما عليك سوى إضافة الكلمة المفتاحية inline قبل إعلان الدالة. يقوم المترجم تلقائيًا باستبدال جسم الدالة في مواقع الاستدعاء. تستمر الدالة نفسها في الوجود في كود البايت للحالات التي لا يتم استدعاؤها فيها مباشرة (على سبيل المثال، من كود Java).
inline fun Int.repeatAction(action: (Int) -> Unit) {
for (i in 0 until this) {
action(i)
}
}
// استدعاء — يتم تضمين كود الامدا في جسم الدالة
5.repeatAction { index ->
println("Index: $index")
}
بعد الترجمة، سيكون الكود أعلاه مكافئًا لـ:
// ماذا يحدث بعد التضمين (بشكل تخطيطي):
val $this = 5
for (i in 0 until $this) {
println("Index: $i")
}
لا يتم إنشاء كائنات للامدا — يتم تنفيذ كود الإجراء مباشرة. هذا هو جوهر التحسين: بدلاً من استدعاء Function.invoke() — إدراج مباشر للكود مع جسم الامدا.
للتحقق من التضمين، افتح Tools > Kotlin > Show Kotlin Bytecode في IntelliJ IDEA وانقر فوق Decompile. سترى أنه بدلاً من استدعاء repeatAction مع لامدا، يتم إنشاء إدراج مباشر لجسم الدالة مع حلقة for.
يتم ترجمة كل لامدا في Kotlin إلى أحد ثلاثة أشكال. الأول — إذا كانت الامدا لا تلتقط متغيرات، فإنها تصبح طريقة ثابتة للفئة التي تم الإعلان عنها فيها. الثاني — إذا التقطت متغيرًا واحدًا، يتم إنشاء فئة مجهولة. الثالث — إذا التقطت عدة متغيرات، يتم إنشاء فئة مجهولة مع حقول لكل متغير ملتقط.
| نوع الامدا | بدون inline | مع inline |
|---|---|---|
| بدون التقاط | طريقة ثابتة واحدة (معاد استخدامها) | تضمين كامل، بدون استدعاء |
| مع التقاط متغير واحد | فئة مجهولة (كائن واحد) | تضمين كامل، بدون كائن |
| مع التقاط N متغيرات | فئة مجهولة مع N حقول | تضمين كامل، بدون كائن |
| متكررة | استدعاء عادي | inline محظور |
وفقًا لـ Android Performance Patterns (Google, 2024)، في التطبيقات ذات الاستخدام المكثف للمجموعات (التصفية، الترتيب، التجميع) تقلل الدوال المضمنة من التخصيصات بنسبة 25–35%. يكون التأثير ملحوظًا بشكل خاص في Jetpack Compose، حيث يؤدي كل تغيير حالة إلى إعادة تركيب مع العديد من الامدا.
لا يمكن للامدا في دالة عادية إجراء return من الدالة الخارجية — فقط return محلي من الامدا نفسها (عبر return@label). في دالة مضمنة، يتم تضمين الامدا في جسم الدالة المستدعية، لذا يصبح الإرجاع غير المحلي ممكنًا: return داخل الامدا ينهي الدالة الخارجية.
inline fun findFirst(
items: List<Int>,
predicate: (Int) -> Boolean
): Int {
for (item in items) {
if (predicate(item)) {
return item
}
}
return -1
}
fun processNumbers() {
val numbers = listOf(1, 2, 3)
val firstEven = findFirst(numbers) { it % 2 == 0 }
// return في الامدا سيعيد null من processNumbers()
}
الإرجاع غير المحلي مناسب للإنهاء المبكر، ولكنه قد يؤدي إلى أخطاء. إذا تم استخدام الامدا في سياق غير محلي (مخزنة في متغير)، فسيؤدي الإرجاع غير المحلي إلى RuntimeException. يصدر مترجم Kotlin تحذيرًا عند محاولة هذا التخزين.
عندما تحتوي الدالة على عدة معاملات لامدا، قد يكون من الضروري تضمين بعضها فقط. لهذا يُستخدم noinline — فهو يمنع تضمين معامل لامدا معين، ويتركه ككائن Function عادي.
معدّل crossinline يحل المشكلة المعاكسة: يتم تضمين الامدا، ولكن يُحظر الإرجاع غير المحلي. هذا ضروري عند استخدام الامدا داخل لامدا أخرى أو في سياق لا يُسمح فيه بـ return (على سبيل المثال، تمريرها إلى Runnable).
inline fun processWithCallback(
data: String,
crossinline onSuccess: (String) -> Unit,
noinline onError: (Exception) -> Unit
) {
try {
val result = process(data)
onSuccess(result)
} catch (e: Exception) {
onError(e)
}
}
// noinline: يمكن تخزين onError في متغير أو تمريره إلى مكان آخر
val errorHandler = { e: Exception -> log(e.message) }
processWithCallback("input", { println(it) }, errorHandler)
في المثال، onSuccess موسومة كـ crossinline — سيتم تضمينها، ولكن لا يمكن استخدام return داخلها. onError موسومة كـ noinline — لا يتم تضمينها، لذا يمكن تمريرها ككائن، أو تخزينها في حقل فئة، أو استخدامها كمستمع.
للدوال المضمنة قيود. الدوال المضمنة المتكررة محظورة — سيرجع المترجم خطأً. لا يمكن أن تتمتع الدوال المضمنة برؤية private أو internal إذا تم الإعلان عنها في وحدة أخرى، ولكن هذا قيد رؤية، لا علاقة له بآلية التضمين نفسها.
يزداد حجم كود البايت مع كل استدعاء لدالة مضمنة، حيث يتم نسخ الجسم. وفقًا لـ Kotlin Coding Conventions (JetBrains, 2025)، يُوصى باستخدام inline فقط للدوال حتى 10–15 سطرًا. بالنسبة للدوال الكبيرة، قد يتم إلغاء فائدة تضمين الامدا بسبب زيادة حجم APK (أمر بالغ الأهمية في Android بسبب حد 64K أسلوب).
// الممارسة الموصى بها
inline fun withLock(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
} finally {
lock.unlock()
}
}
// غير موصى به للدوال الكبيرة
inline fun largeComputation(...) { // سيء — الجسم >50 سطرًا
// أكثر من 50 سطرًا — من الأفضل استخراجها في دالة عادية
}
تتطلب الدوال المضمنة العامة في المكتبات الحذر: إذا تغير جسم دالة مضمنة، يجب إعادة ترجمة جميع العملاء. توصي JetBrains باستخدام @PublishedApi internal للأعضاء الذين يتم استدعاؤهم من دوال مضمنة للحفاظ على التوافق داخل الوحدة.
الأسئلة الشائعة
نعم، تعمل دالة الامتداد المضمنة بدون قيود. على سبيل المثال: inline fun String.transform(block: (Char) -> Char): String. لا يؤثر الامتداد على إمكانية التضمين — يتعامل معها المترجم بنفس طريقة التعامل مع الدالة المضمنة العادية.
إذا كانت الدالة لا تقبل معاملات لامدا — لا يوفر inline أي ميزة. يصدر مترجم Kotlin تحذيرًا: “Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types.” أيضًا، inline ضار للدوال الكبيرة بسبب نمو كود البايت.
inline — معدّل دالة يقوم بتضمين جسم الدالة في موقع الاستدعاء. @JvmInline (value class) — آلية لفئات الغلاف التي يتم استبدالها بقيمتها في وقت الترجمة. مفهومان مختلفان: inline يحسن الاستدعاءات، value class يحسن تمثيل البيانات.
لا، لا يمكن أن تكون دوال suspend مضمنة لأنها تُترجم إلى آلة حالة مع Continuation. ومع ذلك، يمكن لدالة مضمنة أن تقبل لامدا suspend كمعامل مع crossinline. يُستخدم هذا غالبًا في coroutines: inline fun launch(block: suspend CoroutineScope.() -> Unit).
نعم، الدوال المضمنة تعقّد التصحيح لأنه لا يتم استدعاء جسم الدالة بل يتم تضمينه في موقع الاستدعاء. تصبح تتبعات الاستدعاء أطول، ونقاط التوقف تعمل ولكنها قد تظهر مواضع غير متوقعة. توصي JetBrains بالتصحيح بدون inline وتفعيله فقط في إصدارات release.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا