خدمة الطلبات التي بنيناها في مرحلة Kotlin وصلت إلى مراجعة الكود Code Review، والمراجع هو مطور Android في الفريق الذي يكتب Kotlin منذ سنوات. وعاد طلب الدمج Pull Request بخمس عشرة ملاحظة، ولم تكن أي ملاحظة منها عن منطق الطلبات أو قاعدة البيانات، وإنما كلها من نوع واحد: «هذا يعمل، ولكن هكذا تكتب في Java» أو «هذه عادة من Python ستعطيك نتيجة خاطئة في Kotlin دون أي خطأ». وبعض الملاحظات كانت عن أسلوب، وبعضها كان عن أخطاء حقيقية مختبئة: سلة لا تستطيع أن تحذف منها منتجاً، وقائمة «ثابتة» تغيرت، وروتين ألغي ولم يتوقف، وبريد يرسل بعد أن انتهى الطلب.
وهذا المقال هو الأخير في المرحلة، وهو مرجع تعود إليه أكثر مما تقرؤه مرة واحدة: خمسة عشر خطأً يقع فيها القادمون إلى Kotlin من لغات أخرى، وكل خطأ منها كتبناه وشغلناه فعلاً، والمخرج المعروض هو المخرج الحقيقي، ثم الإصلاح بالأسلوب الذي يكتبه من يعمل باللغة كل يوم، أي Idiomatic Kotlin. والكود كله في ملف واحد lab/src/main/kotlin/idioms/Mistakes.kt، يعمل على Kotlin 2.4.21 وJDK 25. وفي آخر المقال خطة قراءة من المصادر الرسمية لمن يريد أن يكمل بعد هذه المرحلة.
وسوف نناقش في هذا المقال ما يلي:
- لماذا نكتب Kotlin بلهجة لغتنا الأولى، وكيف تراجع كود زميل قادم من لغة أخرى.
- القيم الفارغة والمساواة:
!!، والمصفوفات داخلdata class. - المجموعات Collections:
varداخلHashSet، والقائمة للقراءة فقط التي تتغير، والنسخ السطحي فيcopy(). - التحكم في التدفق:
returnداخلforEach، وفخ?.let { } ?:، وelseعلى نوعsealed، ودالة الامتداد التي لا تستدعى أبداً. - الأرقام والنصوص:
Doubleللمال، وترتيب القسمة في النسب، وجمع النصوص في حلقة. - التوافق مع Java: القيم الافتراضية التي لا يراها كود Java.
- الروتينات:
runCatchingالتي تبتلع الإلغاء، وGlobalScope. - ماذا تكشف الأدوات من هذه الأخطاء: تحذيرات المترجم وأداة detekt.
- جدول يجمع الأخطاء ومصدرها وإصلاحها، ثم خطة قراءة من المصادر الرسمية.
لماذا نكتب Kotlin بلهجة لغتنا الأولى؟ Mental Models
قبل الأخطاء نفسها، يستحق السؤال وقفة: لماذا يقع مطور خبير في أخطاء من هذا النوع، وهو يعرف البرمجة جيداً؟ والإجابة أن كل لغة تعلمك نموذجاً ذهنياً Mental Model لما يحدث تحت الكود، وهذا النموذج يعمل في الخلفية دون أن تفكر فيه. فالقادم من Java يفكر في المراجع والكائنات والاستثناءات المفحوصة، ويرى !! كأنها افتراض عادي بأن القيمة موجودة، لأنه عاش سنوات يفترض ذلك في كل سطر. والقادم من Python يفكر في الأسماء والقيم والقوائم المرنة، ويتوقع أن القسمة تعطي نتيجة عشرية، وأن ما لا يمكن تعديله لا يتغير. والقادم من JavaScript يفكر في حلقة الأحداث والدوال التي تعمل لاحقاً دون أن ينتظرها أحد، وفي القيم «الصادقة» و«الكاذبة» التي تجعل a ?? b وa || b اختصاراً لكل شيء.
وKotlin تشبه كل هذه اللغات في الصيغة بما يكفي لتعمل عاداتك القديمة دون أن يعترض المترجم في أغلب الحالات، وتختلف عنها في المعنى بما يكفي لتعطيك نتيجة مختلفة. وهذا بالضبط ما يجعل الأخطاء خطيرة: الكود يترجم، والاختبارات المكتوبة بنفس العقلية تمر، والخطأ يظهر عند عميل يحاول أن يحذف منتجاً من سلته. ولذلك رتبنا كل خطأ في هذا المقال بنفس الشكل: العادة كما يكتبها القادم من لغة أخرى، ثم ما حدث فعلاً عند التشغيل، ثم لماذا حدث بحسب نموذج Kotlin، ثم الإصلاح. والهدف ليس أن تحفظ خمسة عشر قاعدة، وإنما أن تستبدل النموذج الذهني القديم بنموذج Kotlin، فتتوقع الخطأ القادم قبل أن تكتبه.
كيف بنينا المختبر Lab؟
كل خطأ في المختبر دالة أو اثنتان: النسخة الخاطئة كما يكتبها القادم من لغة أخرى، والنسخة المصلحة. والدالة main تشغل الأخطاء بالترتيب، وتطبع لكل واحد ما يحدث، وإذا رمى خطأ استثناء فإنها تطبعه مع أول سطر من مسار الاستدعاء وتكمل إلى التالي. وشغلناه داخل Container من الـ Image gradle:9.8.2-jdk25-noble بالأمرين gradle installDist ثم build/install/idioms/bin/idioms، والمخرج الكامل في lab/output-main.txt. وبجانب ملف Kotlin يوجد كلاس Java صغير في lab/src/main/java/idioms/LegacyCaller.java يستدعي كود Kotlin، لأن الخطأ الرابع عشر لا يظهر إلا عندما يترجم كود Java في نفس المشروع. وثلاث تجارب إضافية غيرت الكود داخل الـ Container قبل البناء، دون أن تلمس الملفات الأصلية: كتابة when دون else، واستدعاء دالة بقيمة افتراضية من Java دون @JvmOverloads، وتشغيل المترجم بخيار يجعل التحذيرات أخطاء، ومعها تشغيل detekt، وكل واحدة لها ملف مخرج باسمها في نفس المجلد. وأول ما طبعه البناء، قبل أي تشغيل، كان تحذيراً من المترجم سوف نعود إليه في الخطأ العاشر:
w: file:///w/src/main/kotlin/idioms/Mistakes.kt:94:11 This extension is shadowed by a member: 'fun format(): String' defined in 'idioms.Price'.والصورة التالية تجمع الأخطاء الخمسة عشر بحسب اللغة التي تأتي منها العادة غالباً، وهذا التصنيف تقريبي، فبعض الأخطاء يقع فيها الجميع:

القيم الفارغة Null والمساواة Equality
الخطأ 1: العلامة !! Not-Null Assertion بدل ?: مع رسالة
القادم من Java يرى أن المترجم يعترض على قراءة Map، لأن النتيجة Int?، فيكتب !! ليسكته، كما كان يفترض في Java أن المفتاح موجود:
val stockById = mapOf(1 to 10, 2 to 0)
fun m1Wrong(id: Int): Int = stockById[id]!!
fun m1Fixed(id: Int): Int = stockById[id] ?: error("product $id is not in the catalog")m1Fixed(9): java.lang.IllegalStateException: product 9 is not in the catalog
Exception: java.lang.NullPointerException
at idioms.MistakesKt.m1Wrong(Mistakes.kt:15)والفرق بين السطرين هو الفرق بين ساعة تصحيح وثانية واحدة. فالنسخة الخاطئة رمت NullPointerException بلا أي رسالة، ولا تعرف منها ما الذي كان null ولا لماذا، أما النسخة المصلحة فرمت IllegalStateException تقول إن المنتج 9 غير موجود في الكتالوج. والقاعدة: !! هي الطريقة الوحيدة في Kotlin لصنع NullPointerException بيدك، فلا تكتبها في كود الإنتاج، واستخدم ?: error("...") أو ?: return أو getValue() التي ترمي NoSuchElementException برسالة تذكر المفتاح. وتشرح صفحة Null safety هذه الأدوات، وقد رأينا في المقال الثاني من المرحلة أنواع المنصة Platform Types التي هي الباب الآخر لهذا الاستثناء.
الخطأ 5: مصفوفة Array داخل data class
نبدأ بالخطأ الخامس هنا لأنه عن المساواة أيضاً. القادم من Java يكتب سلة بمصفوفة من أرقام المنتجات، لأن المصفوفة في Java هي الشكل الطبيعي لقائمة ثابتة الطول:
data class Basket(val ids: Array<Int>)
data class BasketFixed(val ids: List<Int>)Basket(arrayOf(1,2)) == Basket(arrayOf(1,2)) -> false
toString: Basket(ids=[1, 2])
BasketFixed(listOf(1,2)) == BasketFixed(listOf(1,2)) -> trueسلتان فيهما نفس المنتجين، و== تقول إنهما مختلفتان. والسبب أن data class تولد equals بمقارنة كل خاصية بـ equals الخاصة بها، والمصفوفة على الـ JVM لا تعيد تعريف equals، فهي تقارن المرجع، وتشرح صفحة Arrays ذلك وتوصي بتجنب المصفوفات في data class. ولاحظ أن toString يطبع المحتوى بشكل صحيح، فالخطأ يبدو مستحيلاً عند القراءة في السجل. والإصلاح أن تستخدم List كما في BasketFixed، وتترك المصفوفات للأداء في الحسابات الرقمية الكبيرة كما رأينا في المقال الثاني (IntArray)، وإذا احتجتها داخل data class فاكتب equals وhashCode بنفسك بـ contentEquals.
المجموعات Collections: ما تراه ليس دائماً ما في الذاكرة
الخطأ 2: data class فيها var داخل HashSet
سطر في السلة له رقم منتج وكمية، والكمية var لأنها تتغير، والسطور في HashSet حتى لا يتكرر المنتج:
data class CartLine(val productId: Int, var qty: Int)
val cart = hashSetOf(CartLine(1, 1))
val line = cart.first()
line.qty = 3 // نعدل الكائن وهو داخل الـ Set
println("cart = $cart")
println("cart.contains(line) = ${cart.contains(line)}")
println("cart.remove(line) = ${cart.remove(line)}, size after remove = ${cart.size}")cart = [CartLine(productId=1, qty=3)]
cart.contains(line) = false
cart.remove(line) = false, size after remove = 1وهذا من أغرب ما يمكن أن تراه: السلة تطبع السطر، والسطر هو نفس الكائن الذي فيها، وcontains تقول إنه غير موجود، وremove تفشل، فلا يستطيع العميل أن يحذف المنتج من سلته. والسبب أن HashSet وضعت الكائن في خانة بحسب hashCode وقت الإضافة، وdata class تحسب hashCode من كل الخصائص بما فيها qty، فلما تغيرت الكمية تغير الهاش، وصار البحث يذهب إلى خانة أخرى فارغة. والكائن نفسه ما زال في الخانة القديمة، يظهر عند الطباعة والمرور على المجموعة، ولا يجده البحث أبداً. والصورة التالية تبين ذلك:

والإصلاح بأسلوب Kotlin أن تكون خصائص data class كلها val، وأن تغير الكمية بإنشاء نسخة جديدة بـ copy(qty = 3)، فتحذف القديمة وتضيف الجديدة. وأفضل من ذلك في هذه الحالة أن تكون السلة Map<Int, Int> من رقم المنتج إلى الكمية، فالمفتاح لا يتغير أبداً. والقاعدة التي تكتبها صفحة Data classes بوضوح: data class للقيم، والقيم لا تتغير.
ونفس المشكلة تظهر بشكل أخطر في كود قاعدة البيانات: إذا كتبت كيان Entity لمكتبة مثل Hibernate على شكل data class، فإن hashCode يتغير عندما تعطيه قاعدة البيانات رقمه بعد الحفظ، أو عندما تحمل المكتبة علاقاته لاحقاً، فيضيع الكائن من أي Set وضعته فيه قبل الحفظ. ولهذا السبب توصي أدلة المكتبات التي تعمل مع Kotlin بأن تكون الكيانات كلاسات عادية تكتب equals وhashCode على المعرف وحده، وتترك data class للقيم التي تنقل بين الطبقات، مثل نماذج JSON التي رأيتها في مقال الخدمة.
الخطأ 3: القائمة List للقراءة فقط ليست ثابتة Immutable
القادم من Python يقرأ أن List في Kotlin لا تقبل add، فيفهم أنها مثل tuple، أي أنها لا تتغير أبداً. والكود التالي يعطي قائمة الأسعار لجزء آخر من البرنامج على أنها List:
val backing = mutableListOf(990, 3250)
val prices: List<Int> = backing // نعطيها لكود آخر كـ List
backing.add(1575) // ثم يتغير المصدر
println("prices seen as List: $prices")
val fixed = listOf(990, 3250)
try { (fixed as MutableList<Int>).add(1575) } catch (e: UnsupportedOperationException) { println("listOf + cast to MutableList: $e") }
val copy = backing.toList() // نسخة حقيقية
backing.add(500)
println("toList() copy after another add: $copy")prices seen as List: [990, 3250, 1575]
listOf + cast to MutableList: java.lang.UnsupportedOperationException
toList() copy after another add: [990, 3250, 1575]في المخرج أعلاه لاحظ التالي:
- السطر الأول هو المشكلة: الكود الذي استلم
pricesكـListرأى عنصراً ثالثاً ظهر فجأة، لأنListفي Kotlin واجهة Interface للقراءة فقط، وليست نوعاً ثابتاً. فهي تمنع صاحب هذا المتغير من التعديل، ولا تمنع أي شخص آخر يملك مرجعاً إلى نفس الكائن من نوعMutableList. وتشرح صفحة Collections overview هذا الفرق بين Read-Only وImmutable. - السطر الثاني يبين أن
listOfمع أكثر من عنصر ترجع على الـ JVM كائناً من Java لا يقبل الإضافة، فالتحويل إلىMutableListنجح في الترجمة ورمىUnsupportedOperationExceptionوقت التشغيل. والتحويل نفسه هو الخطأ، فلا تكتبas MutableListأبداً. - السطر الثالث هو الإصلاح:
toList()تنسخ المحتوى في كائن جديد، فلم يتأثر بالإضافة اللاحقة.
والقاعدة العملية: إذا كنت تعطي قائمة لكود آخر وتريد أن تضمن أنها لن تتغير تحته، فأعطه نسخة بـ toList()، أو استخدم مكتبة kotlinx.collections.immutable التي تقدم أنواعاً ثابتة فعلاً. وهذا يهم كثيراً مع الروتينات، فقائمة تقرؤها في روتين ويعدلها روتين آخر هي حالة سباق كما رأيت في مقال التزامن.
الخطأ 4: الدالة copy() نسخة سطحية Shallow Copy
data class Order(val id: Int, val items: MutableList<String>)
val original = Order(1, mutableListOf("قهوة"))
val draft = original.copy()
draft.items += "تمر"
println("original.items = ${original.items}")
println("same list object: ${original.items === draft.items}")
val deep = original.copy(items = original.items.toMutableList())
deep.items += "هيل"
println("after deep copy: original=${original.items} deep=${deep.items}")original.items = [قهوة, تمر]
same list object: true
after deep copy: original=[قهوة, تمر] deep=[قهوة, تمر, هيل]والخطأ في السطرين الأولين: عدلنا مسودة الطلب draft بإضافة التمر، فتغير الطلب الأصلي original أيضاً، والسطر same list object: true يشرح السبب: copy() تنسخ الخصائص نفسها، أي المراجع، ولا تنسخ ما تشير إليه، فالكائنان يشتركان في نفس القائمة. والسطر الثالث هو النسخ العميق: أعطينا copy قائمة جديدة بـ toMutableList()، فأخذت محتوى الأصل في تلك اللحظة (القهوة والتمر)، ثم أضفنا الهيل إليها وحدها، وبقي الأصل [قهوة, تمر]. والإصلاح الأفضل ليس النسخ العميق، وإنما ألا تكون الخاصية MutableList أصلاً: اجعلها List، وأضف بإنشاء نسخة جديدة copy(items = items + "تمر")، فلا يوجد شيء مشترك قابل للتعديل.

copy() كلاهما يشارك المرجع: من يملك مرجعاً قابلاً للتعديل يغير ما يراه الآخرونالتحكم في التدفق Control Flow: أقواس تبدو بريئة
الخطأ 6: return داخل forEach
القادم من Java يكتب forEach كأنها حلقة for، ويستخدم return ليتخطى عنصراً كما يستخدم continue:
fun logAll(stock: List<Int>): List<String> {
val out = mutableListOf<String>()
stock.forEach { if (it == 0) return@forEach; out += "ok $it" } // continue
out += "done"
return out
}
fun logAllWrong(stock: List<Int>): List<String> {
val out = mutableListOf<String>()
run { stock.forEach { if (it == 0) return out; out += "ok $it" } } // قصدنا continue وكتبنا return
out += "done"
return out
}logAll -> [ok 5, ok 7, done]
logAllWrong -> [ok 5]
firstOutOfStockWrong -> item 1 is out of stockالنسخة الخاطئة توقفت عند أول مخزون صفر، ولم تطبع 7 ولا «done». والسبب أن forEach دالة inline كما رأينا في المقال الثاني، فجسمها ينسخ داخل الدالة المحيطة، وreturn العادية داخلها ترجع من الدالة المحيطة كلها، وهذا اسمه الرجوع غير المحلي Non-Local Return. والرجوع إلى الدالة المجهولة فقط، أي ما يقابل continue، يكتب return@forEach كما تشرح صفحة Returns and jumps. والسطر الثالث يبين أن الرجوع غير المحلي مفيد عندما تقصده، فدالة «أول منتج نفد» تخرج عند أول نتيجة. والقاعدة: إذا احتجت break أو continue فاكتب حلقة for عادية، فهي أوضح للقارئ من أي علامة.
الخطأ 7: فخ ?.let { } ?: الفرع البديل
وهذا من أشهر أخطاء القادمين من JavaScript الذين يعاملون ?.let { } ?: ... كأنها if/else مختصرة. الدالة audit تسجل الاسم وترجع null أحياناً (هنا: للأسماء الطويلة):
fun audit(msg: String): String? = if (msg.length > 5) null else "logged:$msg" // ترجع null أحياناً
fun m7Wrong(name: String?): String = name?.let { audit(it) } ?: "name is missing"
fun m7Fixed(name: String?): String = if (name != null) audit(name) ?: "audit skipped" else "name is missing"m7Wrong("Sara") -> logged:Sara
m7Wrong("Abdullah") -> name is missing
m7Fixed("Abdullah") -> audit skipped
m7Wrong(null) -> name is missingالاسم «Abdullah» موجود وليس null، والنسخة الخاطئة قالت «name is missing». والسبب أن ?: لا تسأل «هل name null؟» وإنما «هل نتيجة كل ما على اليسار null؟»، وlet ترجع نتيجة الـ Lambda، والـ Lambda رجعت null من audit، فعمل الفرع البديل. والنتيجة رسالة خاطئة في السجل وفي الرد، وهذا الخطأ يمر في الاختبار بأي اسم قصير. والإصلاح أن تكتب if صريحة عندما يكون الفرعان مختلفين في المعنى، وتترك ?.let للتحويل البسيط الذي لا يرجع null. وتشرح صفحة Scope functions متى تستخدم let وalso وapply وrun، وأهم سطر فيها أن also ترجع الكائن نفسه وليس نتيجة الـ Lambda، فـ name?.also { audit(it) } ?: ... لا يقع في هذا الفخ.

?: يسأل عن نتيجة السلسلة كلها، وليس عن المتغير الأولالخطأ 9: else على نوع sealed يخفي الحالة الجديدة
طرق الدفع نوع مغلق، والدالة feeWrong تحسب رسوم الدفع: البطاقة 150 هللة، وكل ما عداها صفر، فكتب المطور else كما يكتب default في switch. ثم أضاف فريق آخر نوعاً ثالثاً هو المحفظة الإلكترونية، ورسومها في العقد ليست صفراً:
sealed interface Payment {
data class Card(val last4: String) : Payment
data class Cash(val change: Int) : Payment
data class Wallet(val provider: String) : Payment // نوع جديد أضيف لاحقاً
}
fun feeWrong(p: Payment): Int = when (p) {
is Payment.Card -> 150
else -> 0 // Cash لا رسوم... و Wallet أيضاً بالخطأ
}Card: fee=150
Cash: fee=0
Wallet: fee=0والبناء نجح، والتشغيل نجح، والمحفظة صارت بلا رسوم دون أن يعرف أحد. ثم جربنا «ماذا يحدث إذا» كتبنا الدالة بالأسلوب الصحيح، أي كل حالة باسمها دون else، على نفس الكود الذي فيه Wallet:
84:fun feeWrong(p: Payment): Int = when (p) {
85- is Payment.Card -> 150
86- is Payment.Cash -> 0
87-}
e: file:///w/src/main/kotlin/idioms/Mistakes.kt:84:33 'when' expression must be exhaustive. Add the 'is Wallet' branch or an 'else' branch.
> Task :compileKotlin FAILEDفالمترجم رفض البناء، وقال بالاسم إن الحالة Wallet غير مغطاة. وهذا هو كل الهدف من sealed كما تشرح صفحة Sealed classes: أن يتحول نسيان حالة جديدة من خطأ مالي صامت إلى خطأ ترجمة. وelse على نوع مغلق تلغي هذه الحماية بالكامل، فلا تكتبها إلا إذا كانت الحالات الأخرى كلها فعلاً لها نفس المعنى الآن وفي المستقبل.
الخطأ 10: دالة امتداد Extension بنفس اسم دالة Member في الكلاس
القادم من JavaScript أو Ruby قد يظن أن دالة الامتداد Extension Function تعيد تعريف الدالة الموجودة، كما يفعل تعديل الـ Prototype. فالكلاس Price فيه format() تطبع الهللات، وأراد مطور أن يغيرها لتطبع بالريال، فكتب دالة امتداد بنفس الاسم:
class Price(val halalas: Int) {
fun format(): String = "$halalas halalas"
}
fun Price.format(): String = "%d.%02d SAR".format(halalas / 100, halalas % 100) // لا تستدعى أبداً
fun Price.formatSar(): String = "%d.%02d SAR".format(halalas / 100, halalas % 100)Price(2970).format() -> 2970 halalas
Price(2970).formatSar() -> 29.70 SARودالة الامتداد لم تستدع أبداً، لأن القاعدة في Extensions أن دالة الكلاس نفسه Member تفوز دائماً عند تساوي الاسم والمعاملات. والمترجم حذرنا من ذلك وقت البناء بالسطر This extension is shadowed by a member الذي رأيته في أول المقال، وهذا سبب آخر لجعل التحذيرات أخطاء في خط CI. والإصلاح اسم مختلف كما في formatSar، أو تعديل الكلاس نفسه إذا كنت تملكه.
الأرقام والنصوص Numbers and Strings
الخطأ 8: Double للمال
وهذا خطأ عام في كل اللغات، ولكنه يتكرر في Kotlin لأن 9.90 تكتب بسهولة وتبدو كسعر:
0.1 + 0.2 = 0.30000000000000004
three items at 0.10 SAR: 0.30000000000000004
9.90 * 3 * 0.15 = 4.455
halalas: 990 * 3 = 2970, vat = 446ثلاث قطع بعشر هللات صارت 0.30000000000000004 ريال، لأن 0.1 لا يمكن تمثيلها بالضبط في النظام الثنائي، وهذا ما شرحناه بالتفصيل في مقال الأرقام العشرية Floating Point من الداخل. والسطر الثالث خادع أكثر، فالضريبة 4.455 ظهرت صحيحة، ولكنها في الذاكرة قد تكون أقل قليلاً أو أكثر قليلاً من 4.455، فإذا قربتها إلى منزلتين فقد تصبح 4.45 أو 4.46 بحسب الخطأ الصغير المخفي، وهذا بالضبط الفرق بين الفاتورة والمحاسب. والسطر الأخير هو الطريقة التي استخدمناها في الخدمة: كل شيء بالهللات Int، والضريبة بالأعداد الصحيحة، والنتيجة 446 بالضبط في كل مرة. وإذا احتجت كسوراً حقيقية، كنسب الخصم، فاستخدم BigDecimal من Java مع RoundingMode محدد.
وإذا انتقلت إلى BigDecimal فانتبه لنقطة واحدة تنقل نفس المشكلة معك: أنشئه من نص مثل BigDecimal("9.90") أو من عدد صحيح بالهللات مع منزلتين عشريتين، ولا تنشئه من Double مثل 9.90، لأن الرقم العشري يحمل خطأه الثنائي إلى داخل BigDecimal قبل أن تبدأ. وحدد دائماً RoundingMode في كل قسمة، لأن قسمة بلا نهاية مثل الثلث ترمي استثناء دون نمط تقريب محدد.
الخطأ 15: ترتيب القسمة الصحيحة Integer Division في النسب
وبعد أن تعلم المطور القادم من Python أن يحسب بالهللات، وقع في الخطأ المعاكس: حساب نسبة الخصم بالأعداد الصحيحة بترتيب يبدو منطقياً على الورق، أي «اقسم على مئة ثم اضرب في النسبة»:
fun discountWrong(price: Int, percent: Int): Int = price / 100 * percent
fun discountFixed(price: Int, percent: Int): Int = (price * percent + 50) / 100price=990 15%: wrong=135 fixed=149
price=99 15%: wrong=0 fixed=15
price=3250 15%: wrong=480 fixed=488خصم 15% على علبة قهوة بـ 9.90 ريال صار 1.35 ريال بدل 1.49، وعلى منتج بـ 99 هللة صار صفراً. والسبب أن 990 / 100 في Kotlin تساوي 9 وليس 9.9، فالقسمة الصحيحة حذفت الجزء العشري قبل الضرب، ثم ضرب الخطأ في 15. وفي Python 3 تعطي / رقماً عشرياً دائماً، و// هي القسمة الصحيحة، ولذلك لا يتوقع القادم منها أن العلامة نفسها تتصرف بشكلين في Kotlin بحسب نوع الطرفين. والقاعدة مع الأعداد الصحيحة: اضرب أولاً ثم اقسم، وأضف نصف المقسوم عليه قبل القسمة إذا أردت التقريب للأقرب، كما فعلنا في دالة الضريبة في الخدمة، وانتبه للتجاوز عند الضرب في أرقام كبيرة، وهنا يأتي دور Math.multiplyExact. ولاحظ أن هذا الخطأ لا يرمي شيئاً ولا يحذر منه المترجم، والاختبار الوحيد الذي يكشفه اختبار بأرقام حقيقية لها جزء عشري، مثل 990 و99، وليس بأرقام مدورة مثل 1000.
الخطأ 13: جمع النصوص String Concatenation في حلقة
القادم من Python أو JavaScript يبني نص الفاتورة بـ += في حلقة، وهذا شكل الفاتورة بعشرين ألف سطر:
fun receiptWrong(n: Int): Int { var s = ""; for (i in 0 until n) s += "line $i\n"; return s.length }
fun receiptFixed(n: Int): Int = buildString { for (i in 0 until n) append("line ").append(i).append('\n') }.lengths += in a loop (20,000 lines) 2930 ms result=208890
buildString (20,000 lines) 6 ms result=208890
s += in a loop (20,000 lines) 515 ms result=208890
buildString (20,000 lines) 3 ms result=208890نفس النص بنفس الطول 208,890 حرفاً، و+= أخذت 2.9 ثانية في الجولة الأولى و515 جزءاً من الألف في الثانية، بينما buildString أخذت 3 إلى 6. والسبب أن String على الـ JVM ثابت، فكل += تنشئ نصاً جديداً وتنسخ فيه كل ما سبق، فالعمل يكبر مع مربع عدد الأسطر. وbuildString تستخدم StringBuilder واحداً يكبر عند الحاجة. وفي Python يبدو += سريعاً أحياناً بسبب تحسين خاص في CPython، ولذلك تتعلم العادة هناك ثم تدفع ثمنها هنا. والفرق بين الجولتين هو الإحماء نفسه الذي رأيناه في كل المرحلة.
وفي حالات كثيرة لا تحتاج حتى buildString، فإذا كان النص قائمة عناصر بفاصل فإن lines.joinToString("\n") { "line $it" } تقوم بنفس العمل في سطر واحد وبنفس الكفاءة، لأنها تستخدم StringBuilder داخلياً. والقالب النصي الواحد مثل "$a $b $c" لا مشكلة فيه أصلاً، فالمترجم يحوله إلى بناء واحد، والمشكلة فقط في += المتكرر داخل حلقة.
التوافق مع Java Interop
الخطأ 14: قيم افتراضية Default Arguments لا يراها كود Java
وهذا خطأ يقع فيه من تعلم Kotlin ثم دخل مشروعاً مختلطاً: كتب دالة سعر بالضريبة بمعامل اختياري للنسبة، وهي تعمل من كل كود Kotlin بمعامل واحد، ثم استدعاها زميل من كلاس Java قديم في نفس المشروع:
fun priceWithVat(halalas: Int, vatPercent: Int = 15): Int = halalas + (halalas * vatPercent + 50) / 100
@JvmOverloads
fun priceWithVatJ(halalas: Int, vatPercent: Int = 15): Int = priceWithVat(halalas, vatPercent)public class LegacyCaller {
public static int total() {
return MistakesKt.priceWithVat(990);
}
}> Task :compileJava FAILED
/w/src/main/java/idioms/LegacyCaller.java:6: error: method priceWithVat in class MistakesKt cannot be applied to given types;
required: int,int
found: int
reason: actual and formal argument lists differ in length
BUILD FAILED in 47sومترجم Java رفض الاستدعاء، لأن القيم الافتراضية مفهوم في مترجم Kotlin وحده: الـ Bytecode فيه دالة واحدة بمعاملين، ومعها دالة مساعدة مخفية تستخدمها Kotlin فقط. والعلامة @JvmOverloads تجعل المترجم يولد نسخة إضافية لكل معامل اختياري، فاستدعاء MistakesKt.priceWithVatJ(990) من Java ترجم وأعطى:
Java caller: LegacyCaller.total() = 1139أي 990 هللة زائد ضريبة 149 هللة. ولاحظ اسم الكلاس MistakesKt في كود Java، فهو الكلاس الذي يولده المترجم لدوال المستوى الأعلى في الملف Mistakes.kt كما رأينا في المقال الأول من المرحلة، وتستطيع أن تغيره بالعلامة @file:JvmName("Pricing") حتى يقرأ كود Java بشكل طبيعي. وهذا الخطأ من النوع الجيد، لأنه خطأ ترجمة وليس نتيجة خاطئة، ولكنه يظهر متأخراً، عندما يحاول فريق آخر استخدام مكتبتك. فإذا كانت دوالك العامة ستستدعى من Java، فراجع ثلاث علامات في صفحة Calling Kotlin from Java: @JvmOverloads للقيم الافتراضية، و@JvmStatic لدوال companion object، و@Throws للاستثناءات كما رأينا في المقال الثاني.
الروتينات المساعدة Coroutines: الإلغاء والنطاق
الخطأ 11: runCatching تبتلع الإلغاء Cancellation
القادم من Python أو JavaScript يحب أن يلف كل استدعاء شبكة بشيء يمسك الأخطاء، وrunCatching تبدو الأداة المثالية. والروتين التالي يجلب السعر خمس مرات، وكل جلب يأخذ ثانية، ونلغيه بعد 100 جزء من الألف من الثانية:
val job = launch {
repeat(5) {
if (fixed) {
try { fetchPrice() } catch (e: CancellationException) { throw e } catch (e: Exception) { println("failed: $e") }
} else {
runCatching { fetchPrice() }.onFailure { println(" runCatching caught: ${it::class.simpleName}") }
}
after++
}
}
delay(100)
job.cancel()
job.join() runCatching caught: JobCancellationException
runCatching caught: JobCancellationException
runCatching caught: JobCancellationException
runCatching caught: JobCancellationException
runCatching caught: JobCancellationException
fixed=false: loop iterations after cancel=5, join took 14 ms
fixed=true: loop iterations after cancel=0, join took 0 msوالنسخة الخاطئة أمسكت إشارة الإلغاء JobCancellationException خمس مرات، وأكملت الحلقة كلها كأن شيئاً لم يحدث، فكل جولة استدعت fetchPrice التي رمت الإلغاء فوراً لأن الروتين ملغى، ثم ابتلعتها runCatching وأكملت. وفي هذا المثال كل جولة بعد الإلغاء سريعة، ولكن لو كان في الحلقة كود حاجز أو كتابة في قاعدة البيانات لنفذ كله بعد أن طلبنا الإلغاء، والطلب الذي انتهت مهلته يستمر في العمل. والنسخة المصلحة تعيد رمي CancellationException قبل أن تمسك بقية الاستثناءات، فتوقفت الحلقة عند أول نقطة. وتشرح صفحة Cancellation and timeouts أن الإلغاء استثناء عادي في ظاهره، ولذلك أي catch (e: Exception) أو runCatching حول كود معلق تحتاج هذا السطر.
الخطأ 12: GlobalScope وعمل يستمر بعد انتهاء الطلب
القادم من JavaScript تعود أن يطلق عملاً في الخلفية دون أن ينتظره، مثل إرسال بريد التأكيد بعد الطلب، فيكتب في Kotlin GlobalScope.launch:
@OptIn(DelicateCoroutinesApi::class)
fun handleRequestWrong(log: MutableList<String>) {
GlobalScope.launch { delay(200); log += "email sent (after the request ended)" }
log += "request handled"
}
suspend fun handleRequestFixed(log: MutableList<String>) = coroutineScope {
launch { delay(200); log += "email sent (inside the request scope)" }
log += "request handled"
}response sent, log=[request handled]
400 ms later, log=[request handled, email sent (after the request ended)]
fixed: response sent, log=[request handled, email sent (inside the request scope)]ولاحظ أولاً أن المترجم نفسه أجبرنا على كتابة @OptIn(DelicateCoroutinesApi::class)، فـ GlobalScope معلمة في المكتبة كواجهة حساسة، وتقول وثائقها إنها لا تستخدم في كود التطبيقات عادة. والمخرج يبين لماذا: الرد ذهب للعميل والسجل فيه «request handled» فقط، والبريد أرسل بعد 200 جزء من الألف، خارج أي نطاق، فلا أحد يلغيه إذا ألغي الطلب، ولا أحد يعرف إذا فشل، وإذا أغلقت الخدمة في تلك اللحظة ضاع دون أثر. والنسخة المصلحة تطلق نفس العمل داخل coroutineScope، فالدالة لا ترجع حتى ينتهي البريد، وأي خطأ فيه يصل إلى المستدعي. وإذا كان المطلوب فعلاً ألا ينتظر العميل البريد، فالحل ليس GlobalScope، وإنما نطاق تملكه الخدمة نفسها بدورة حياتها، أو الأفضل طابور Queue خارجي يضمن الإرسال حتى لو أعيد تشغيل الخدمة.
وفي خدمة Ktor تحديداً، لكل طلب نطاق روتينات خاص به ينتهي بانتهاء الطلب، وللتطبيق كله نطاق ينتهي عند إيقاف السيرفر. فإذا أردت عملاً يستمر بعد الرد، مثل تحديث إحصائية لا تهم العميل، فأطلقه في نطاق التطبيق وليس في GlobalScope، فيلغى عند إيقاف الخدمة بشكل منظم بدل أن يقطع في منتصفه، وتصل أخطاؤه إلى معالج الأخطاء الذي تضبطه أنت. والفرق يبدو صغيراً في الكود، ولكنه الفرق بين عمل له صاحب يعرف متى يبدأ ومتى ينتهي، وعمل يعيش وحده حتى يموت مع العملية.

ماذا تكشف الأدوات؟ تحذيرات المترجم Compiler Warnings وأداة detekt
والسؤال الطبيعي بعد هذه القائمة: أليست هناك أداة تكشف هذه الأخطاء تلقائياً؟ وجربنا الأداتين اللتين رأيناهما في المقال الأول على نفس الملف. الأولى أن نجعل كل تحذير من المترجم خطأً، بإضافة سطر واحد إلى ملف البناء:
kotlin { compilerOptions { allWarningsAsErrors.set(true) } }e: warnings found and -Werror specified
e: file:///w/src/main/kotlin/idioms/Mistakes.kt:94:11 This extension is shadowed by a member: 'fun format(): String' defined in 'idioms.Price'.
> Task :compileKotlin FAILEDفأوقف البناء بسبب الخطأ العاشر وحده، وهذا متوقع، فالمترجم يحذر مما يستطيع أن يثبته من الكود، والخطأ التاسع رفضه المترجم فقط عندما كتبناه دون else. والأداة الثانية هي detekt بالإصدار 2.0.0-alpha.6، لأن الإصدار المستقر 1.23.8 لا يعمل على JDK 25 كما رأينا في المقال الأول، وشغلناها بإعداداتها الافتراضية، وهذا عدد الملاحظات بحسب القاعدة من المخرج الكامل في lab/output-detekt.txt:
> Analysis failed with 77 issues.
65 [MagicNumber]
5 [MaxLineLength]
2 [TooGenericExceptionCaught]
2 [EmptyElseBlock]
1 [TooManyFunctions]
1 [LongMethod]
1 [CyclomaticComplexMethod]والنتيجة درس في حد ذاتها: 77 ملاحظة، أغلبها أرقام سحرية لأن الملف مليء بالأسعار والكميات، ولم تكشف أي قاعدة من القواعد الافتراضية !! ولا var في data class ولا المصفوفة فيها ولا GlobalScope. وأغرب ما فيها أن القاعدة TooGenericExceptionCaught أشارت إلى السطر 106، وهو النسخة المصلحة من الخطأ الحادي عشر التي تمسك Exception بعد أن تعيد رمي الإلغاء، وأن EmptyElseBlock أشارت إلى سطرين فيهما if (...) return@forEach ولا يوجد فيهما else أصلاً، وهذا على الأرجح من أخطاء النسخة التجريبية. وفي detekt قواعد أخرى لبعض هذه الأخطاء، مثل قاعدة الاستدعاء غير الآمن على نوع يقبل null، ولكنها غير مفعلة افتراضياً، فتحتاج ملف إعداد تفعلها فيه وتطفئ الضجيج مثل MagicNumber في ملفات الاختبار. والخلاصة العملية: الأدوات تكشف القليل من هذه الأخطاء بإعداداتها الافتراضية، وأكثرها يكشفه مراجع يعرف اللغة، أو اختبار بأرقام حقيقية، أو هذه القائمة.
كيف تراجع كود Kotlin لزميل قادم من لغة أخرى؟ Code Review
والقائمة السابقة مفيدة لك عندما تكتب، ولكنها أكثر فائدة عندما تراجع كود غيرك، فالمراجع في قصتنا لم يكتشف الأخطاء لأنه أذكى، وإنما لأنه يعرف أين يبحث. والطريقة العملية التي نقترحها لمراجعة كود مطور جديد على Kotlin هي أن تقرأ الكود ثلاث مرات بثلاث عيون مختلفة، بدل قراءة واحدة تحاول فيها أن ترى كل شيء:
- قراءة الأنواع: ابحث عن كل
!!وكلasوكلvarداخلdata classوكلMutableListفي توقيع دالة عامة، فهذه أماكن يقول فيها الكود «ثق بي» للمترجم، وكل واحدة منها تحتاج سبباً مكتوباً. - قراءة التدفق: ابحث عن
returnداخل الأقواس المعقوفة، وعن?:بعد سلسلة طويلة، وعنelseفيwhen، واسأل في كل مرة: ماذا يحدث في الحالة التي لم يفكر فيها الكاتب؟ - قراءة الروتينات: ابحث عن
GlobalScopeوrunBlockingوcatch (e: Exception)وrunCatchingفي أي دالةsuspend، وعن أي كود حاجز خارجDispatchers.IO.
وكل ملاحظة في المراجعة تكتب بنفس الشكل الذي استخدمناه في هذا المقال، أي ما الذي سوف يحدث وليس ما هو «الأسلوب الصحيح» فقط: «إذا تغيرت الكمية بعد الإضافة فلن تستطيع حذف السطر من الـ HashSet» تقنع الكاتب أكثر بكثير من «اجعل الخاصية val». والأفضل أن ترفق مع الملاحظة اختباراً صغيراً يفشل، فهذا يحول النقاش من رأي إلى حقيقة. وبعد شهر أو شهرين يصبح الزميل الجديد هو من يكتب هذه الملاحظات للقادم بعده، وهذا هو المؤشر الحقيقي على أن الفريق انتقل إلى Kotlin، وليس عدد الملفات التي تنتهي بـ .kt.
جدول الأخطاء Mistakes Table: المصدر والعرض والإصلاح
| # | الخطأ | العادة من | ما ظهر في التشغيل | الإصلاح |
|---|---|---|---|---|
| 1 | !! | Java | NullPointerException بلا رسالة | ?: error("...") |
| 2 | var في data class داخل HashSet | Java | contains=false وremove=false | كل الخصائص val، أو Map |
| 3 | List على أنها ثابتة | Python | القائمة تغيرت تحت المستلم | toList() عند التسليم |
| 4 | copy() مع خاصية قابلة للتعديل | الجميع | المسودة غيرت الأصل | خصائص List وcopy(items = items + x) |
| 5 | Array في data class | Java | == تعطي false | List |
| 6 | return داخل forEach | Java | الدالة كلها خرجت | return@forEach أو حلقة for |
| 7 | ?.let { } ?: كـ if/else | JavaScript | «name is missing» لاسم موجود | if صريحة أو also |
| 8 | Double للمال | الجميع | 0.30000000000000004 | الهللات Int أو BigDecimal |
| 9 | else على sealed | Java | المحفظة بلا رسوم بصمت | كل حالة باسمها |
| 10 | امتداد باسم دالة موجودة | JavaScript | الامتداد لم يستدع، وتحذير وقت البناء | اسم مختلف |
| 11 | runCatching حول كود معلق | Python، JavaScript | خمس جولات بعد الإلغاء | إعادة رمي CancellationException |
| 12 | GlobalScope.launch | JavaScript | البريد بعد انتهاء الطلب | coroutineScope أو طابور |
| 14 | قيمة افتراضية تستدعى من Java | Kotlin إلى مشروع مختلط | compileJava FAILED | @JvmOverloads |
| 15 | price / 100 * percent | Python | خصم 135 بدل 149، وصفر بدل 15 | اضرب ثم اقسم مع التقريب |
| 13 | += على النصوص في حلقة | Python | 2930 مقابل 6 أجزاء من الألف | buildString |
ولاحظ في عمود «ما ظهر في التشغيل» أن ثلاثة أخطاء فقط من خمسة عشر رمت استثناء (الأول، والثالث عند التحويل، والحادي عشر داخلياً)، والمترجم تكلم في ثلاثة (التاسع عندما كتبناه بالشكل الصحيح، والعاشر بتحذير، والرابع عشر عندما ترجم كود Java)، والباقي أعطى نتيجة خاطئة أو بطيئة بصمت. وهذا هو سبب هذا المقال: الأخطاء التي تحتاج أن تعرفها مسبقاً هي التي لا تخبرك بنفسها.
قائمة تحقق Checklist لمراجعة كود Kotlin
- هل يوجد
!!خارج الاختبارات؟ - هل كل خصائص
data classهيval، ولا توجد فيهاArrayولاMutableList؟ - هل كل قائمة تعطى لكود آخر نسخة، ولا يوجد
as MutableList؟ - هل كل
returnداخل Lambda مقصودة، وكلcontinueمكتوبةreturn@labelأو حلقةfor؟ - هل كل
?.let { } ?:فيها Lambda لا ترجع null؟ - هل لا يوجد
elseفيwhenعلى نوعsealed؟ - هل التحذيرات أخطاء في CI (
allWarningsAsErrors)؟ - هل لا يوجد
DoubleأوFloatفي أي مبلغ؟ - هل كل
catchوrunCatchingحول كود معلق تعيد رميCancellationException؟ - هل لا يوجد
GlobalScope، وكل عمل في الخلفية له نطاق يملكه أحد؟ - هل بناء النصوص الطويلة بـ
buildStringأوjoinToString؟ - هل كل نسبة بالأعداد الصحيحة تضرب قبل أن تقسم؟
- هل الدوال العامة التي يستدعيها كود Java عليها
@JvmOverloadsو@Throwsعند الحاجة؟
خطة القراءة Reading Plan من المصادر الرسمية
وكل ما في هذه المرحلة يغطي ما تحتاجه لتبدأ، ولكن اللغة أوسع من خمسة مقالات، والمصادر الرسمية لـ Kotlin من أفضل وثائق اللغات المتاحة، فهي مكتوبة لمطور يعرف البرمجة، ومليئة بأمثلة تعمل في المتصفح. والترتيب التالي هو الذي نقترحه لمطور قادم من لغة أخرى، بحيث يبني كل أسبوع على ما قبله:
- الأسبوع الأول، الجولة الرسمية: ابدأ بـ Kotlin Tour، وهي جولة قصيرة في الأساسيات بتمارين تعمل في المتصفح، ثم صفحة Idioms التي تجمع الأساليب الشائعة في صفحة واحدة، ثم Coding conventions حتى تكتب كما يكتب الآخرون من أول يوم.
- الأسبوع الأول أيضاً، التمارين: Kotlin Koans مجموعة تمارين صغيرة تبدأ بكود ناقص وتطلب منك إكماله حتى تنجح الاختبارات، وهي أسرع طريقة لتعويد يدك على الصيغة.
- إذا كنت قادماً من Java: سلسلة الأدلة الرسمية من Java إلى Kotlin، وأهمها Nullability in Java and Kotlin وCollections in Java and Kotlin وStrings in Java and Kotlin، فهي تأخذ كود Java الذي تعرفه وتكتبه بأسلوب Kotlin سطراً بسطر.
- الأسبوع الثاني، الأجزاء التي تختلف: Null safety، ثم Scope functions لتعرف متى تستخدم
letوapplyوalsoوrun، ثم Collections overview وReturns and jumps التي تشرحreturn@label. - الأسبوع الثالث، التزامن: Coroutines guide كاملاً بالترتيب، وهو أطول جزء وأهمه للخدمات، ثم ارجع إلى مقال التزامن من هذه المرحلة وأعد التجارب.
- الأسبوع الرابع، السيرفر: وثائق Ktor من البداية حتى الاختبار والنشر، ثم أعد بناء خدمة الطلبات من مقال الخدمة الحقيقية دون أن تنظر إلى الكود.
- للمرجع دائماً: مرجع المكتبة الأساسية kotlin-stdlib عندما تبحث عن دالة، ومواصفات اللغة Kotlin Specification عندما تريد الجواب الدقيق لسؤال مثل «متى يعمل Smart Cast بالضبط؟»، وصفحة Learning materials التي تجمع كل المواد الرسمية الأخرى.
تدريب عملي
- غير
CartLineلتكون كل خصائصهاval، واكتب دالةchangeQtyتحذف السطر القديم وتضيف الجديد، وتأكد أنremoveتعمل بعدها. - أضف إلى
build.gradle.ktsالإعدادkotlin { compilerOptions { allWarningsAsErrors.set(true) } }، وأعد البناء، واقرأ ما يرفضه المترجم في ملف الأخطاء. - اكتب نسخة من الخطأ 11 تستخدم
withTimeout(100)بدلcancel()، وتأكد أن النتيجة نفسها، ثم أصلحها. - عد إلى خدمة الطلبات من المقال الرابع، وابحث فيها عن أي من الأخطاء الخمسة عشر.
الخلاصة
وصلنا لنهاية الموضوع ونهاية المرحلة، وأهم ما فيه:
- أغلب أخطاء القادمين إلى Kotlin لا ترمي استثناء: سلة لا يحذف منها، وقائمة «ثابتة» تتغير، ونسخة تعدل الأصل، ورسوم صفر لطريقة دفع جديدة، ورسالة «غير موجود» لاسم موجود.
data classللقيم، فاجعل خصائصهاval، ولا تضع فيها مصفوفات ولا قوائم قابلة للتعديل.Listللقراءة فقط وليست ثابتة، وcopy()سطحية، والإصلاح نسخ صريحة وخصائص غير قابلة للتعديل.returnداخل الدوالinlineترجع من الدالة المحيطة، و?:بعدletتسأل عن نتيجة الـ Lambda، وelseعلىsealedتلغي حماية المترجم.- الإلغاء في الروتينات استثناء يجب أن يمر، و
GlobalScopeعمل بلا صاحب، و+=على النصوص أبطأ بمئات المرات منbuildStringفي عشرين ألف سطر. - القسمة الصحيحة قبل الضرب أعطت خصم 1.35 ريال بدل 1.49، والقيم الافتراضية لا يراها كود Java دون
@JvmOverloads، وأداة detekt بإعداداتها الافتراضية لم تكشف أي خطأ من الخمسة عشر بقاعدة مخصصة له.
وبهذا تكتمل مرحلة لغة Kotlin للمطورين: الأدوات والصيغة، ثم الأنواع والذاكرة والأخطاء، ثم التزامن بالروتينات، ثم خدمة الطلبات الحقيقية، ثم هذا المقال. وهذه المقالات المنشورة تكمل المرحلة:
دليلكيف تختار لغة البرمجة Programming Language لمشروعك؟نختار لغة خدمة حقيقية بأسئلة الفريق والتوظيف والمكتبات والرخصة، ثم نكتبها بأربع لغات ونقيس البناء والحجم والذاكرة والطلبات في الثانية بأنفسنا.
دليلالبرمجة بعدة لغات Polyglot: متى تفيد ومتى تعقد مشروعك؟نستدعي نفس الدالة من Python بخمس طرق، عبر FFI إلى C وGo وعبر HTTP وgRPC، ونقيس كلفة كل حد بين لغتين وتعقيد البناء وما يحدث عندما يتعطل الطرف الآخر.
دليلأنماط البرمجة وأنظمة الأنواع: لماذا تختلف اللغات؟نحل مسألة واحدة بعدة أساليب برمجة، ونرى هل تكتشف اللغة أخطاءك قبل التشغيل أم بعده، وماذا يعني ذلك عند اختيار لغة لمشروعك.
دليلكيف تتعامل لغات البرمجة مع الأخطاء؟لماذا ترمي Java وPython الاستثناءات بينما ترجع Go وRust الخطأ كقيمة، وكيف تمنع الخطأ من أن يختفي بصمت في كودك.
دليلThreads وasync/await: كود لا يتجمد ولا يضيع بياناتهكيف تكتب كوداً يعمل في أكثر من خيط Thread دون أن تضيع بياناته أو يتجمد، ولماذا تظهر هذه الأخطاء على السيرفر ولا تظهر على جهازك.
دليلإدارة الذاكرة: يدوياً أم Garbage Collector أم Ownership؟نقيس بأيدينا في C وGo وPython وRust من يحرر الذاكرة ومتى، وكم يكلف ذلك من الذاكرة والوقت، وما الأخطاء التي تبقى ممكنة في كل طريقة.
دليلقراءة ال Stack Trace وتصحيح الأخطاء Debugging للمبتدئينكيف تقرأ رسالة الخطأ في Python وJavaScript سطراً بسطر، ومتى تستخدم الطباعة أو نقاط التوقف في VS Code، وكيف يجد git bisect التعديل الذي أدخل الخطأ.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتشغيل الأخطاء الخمسة عشر وإصلاحاتها على Kotlin 2.4.21 وkotlinx.coroutines 1.11.0 وOpenJDK 25.0.5 داخل Docker.