> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# أخطاء شائعة وأسلوب Kotlin الصحيح Idiomatic Kotlin
- URL: https://arabroot.io/articles/اخطاء-شائعة-في-kotlin-والاسلوب-الصحيح-idiomatic-kotlin/
- Published: 2026-10-31T10:25:43.000Z
- Updated: 2026-10-31T11:25:08.000Z
- Description: خمسة عشر خطأً يقع فيها القادمون إلى Kotlin من Java وPython وJavaScript، شغلنا كل واحد منها وعرضنا مخرجه الحقيقي ثم الإصلاح، وفي آخر المقال خطة قراءة من المصادر الرسمية للغة.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

خدمة الطلبات التي بنيناها في [مرحلة Kotlin](https://arabroot.io/start/#stage-lang-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، وكل واحدة لها ملف مخرج باسمها في نفس المجلد. وأول ما طبعه البناء، قبل أي تشغيل، كان تحذيراً من المترجم سوف نعود إليه في الخطأ العاشر:

```text
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'.
```

والصورة التالية تجمع الأخطاء الخمسة عشر بحسب اللغة التي تأتي منها العادة غالباً، وهذا التصنيف تقريبي، فبعض الأخطاء يقع فيها الجميع:

![خريطة الأخطاء الخمسة عشر مصنفة بحسب اللغة التي تأتي منها العادة: Java وPython وJavaScript، وأخطاء الروتينات](https://arabroot.io/content/images/2026/10/kotlin-idioms-01-mistakes-map.webp)

خمسة عشر خطأً في خمس مجموعات، ومعظمها لا يرمي أي استثناء ويعطي نتيجة خاطئة بصمت

## القيم الفارغة Null والمساواة Equality

### الخطأ 1: العلامة !! Not-Null Assertion بدل ?: مع رسالة

القادم من Java يرى أن المترجم يعترض على قراءة `Map`، لأن النتيجة `Int?`، فيكتب `!!` ليسكته، كما كان يفترض في Java أن المفتاح موجود:

```kotlin
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")
```

```text
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](https://kotlinlang.org/docs/null-safety.html?ref=arabroot.io) هذه الأدوات، وقد رأينا في المقال الثاني من المرحلة أنواع المنصة Platform Types التي هي الباب الآخر لهذا الاستثناء.

### الخطأ 5: مصفوفة Array داخل data class

نبدأ بالخطأ الخامس هنا لأنه عن المساواة أيضاً. القادم من Java يكتب سلة بمصفوفة من أرقام المنتجات، لأن المصفوفة في Java هي الشكل الطبيعي لقائمة ثابتة الطول:

```kotlin
data class Basket(val ids: Array<Int>)
data class BasketFixed(val ids: List<Int>)
```

```text
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](https://kotlinlang.org/docs/arrays.html?ref=arabroot.io) ذلك وتوصي بتجنب المصفوفات في `data class`. ولاحظ أن `toString` يطبع المحتوى بشكل صحيح، فالخطأ يبدو مستحيلاً عند القراءة في السجل. والإصلاح أن تستخدم `List` كما في `BasketFixed`، وتترك المصفوفات للأداء في الحسابات الرقمية الكبيرة كما رأينا في المقال الثاني (`IntArray`)، وإذا احتجتها داخل `data class` فاكتب `equals` و`hashCode` بنفسك بـ `contentEquals`.

## المجموعات Collections: ما تراه ليس دائماً ما في الذاكرة

### الخطأ 2: data class فيها var داخل HashSet

سطر في السلة له رقم منتج وكمية، والكمية `var` لأنها تتغير، والسطور في `HashSet` حتى لا يتكرر المنتج:

```kotlin
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}")
```

```text
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`، فلما تغيرت الكمية تغير الهاش، وصار البحث يذهب إلى خانة أخرى فارغة. والكائن نفسه ما زال في الخانة القديمة، يظهر عند الطباعة والمرور على المجموعة، ولا يجده البحث أبداً. والصورة التالية تبين ذلك:

![HashSet يضع CartLine بكمية 1 في خانة بحسب الهاش، ثم تتغير الكمية إلى 3 فيبحث contains في خانة أخرى ولا يجده](https://arabroot.io/content/images/2026/10/kotlin-idioms-02-hashset.webp)

الكائن في خانة الهاش القديم، والبحث يذهب إلى خانة الهاش الجديد

والإصلاح بأسلوب Kotlin أن تكون خصائص `data class` كلها `val`، وأن تغير الكمية بإنشاء نسخة جديدة بـ `copy(qty = 3)`، فتحذف القديمة وتضيف الجديدة. وأفضل من ذلك في هذه الحالة أن تكون السلة `Map<Int, Int>` من رقم المنتج إلى الكمية، فالمفتاح لا يتغير أبداً. والقاعدة التي تكتبها صفحة [Data classes](https://kotlinlang.org/docs/data-classes.html?ref=arabroot.io) بوضوح: `data class` للقيم، والقيم لا تتغير.

ونفس المشكلة تظهر بشكل أخطر في كود قاعدة البيانات: إذا كتبت كيان Entity لمكتبة مثل Hibernate على شكل `data class`، فإن `hashCode` يتغير عندما تعطيه قاعدة البيانات رقمه بعد الحفظ، أو عندما تحمل المكتبة علاقاته لاحقاً، فيضيع الكائن من أي `Set` وضعته فيه قبل الحفظ. ولهذا السبب توصي أدلة المكتبات التي تعمل مع Kotlin بأن تكون الكيانات كلاسات عادية تكتب `equals` و`hashCode` على المعرف وحده، وتترك `data class` للقيم التي تنقل بين الطبقات، مثل نماذج JSON التي رأيتها في مقال الخدمة.

### الخطأ 3: القائمة List للقراءة فقط ليست ثابتة Immutable

القادم من Python يقرأ أن `List` في Kotlin لا تقبل `add`، فيفهم أنها مثل `tuple`، أي أنها لا تتغير أبداً. والكود التالي يعطي قائمة الأسعار لجزء آخر من البرنامج على أنها `List`:

```kotlin
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")
```

```text
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](https://kotlinlang.org/docs/collections-overview.html?ref=arabroot.io) هذا الفرق بين Read-Only وImmutable.
- السطر الثاني يبين أن `listOf` مع أكثر من عنصر ترجع على الـ JVM كائناً من Java لا يقبل الإضافة، فالتحويل إلى `MutableList` نجح في الترجمة ورمى `UnsupportedOperationException` وقت التشغيل. والتحويل نفسه هو الخطأ، فلا تكتب `as MutableList` أبداً.
- السطر الثالث هو الإصلاح: `toList()` تنسخ المحتوى في كائن جديد، فلم يتأثر بالإضافة اللاحقة.

والقاعدة العملية: إذا كنت تعطي قائمة لكود آخر وتريد أن تضمن أنها لن تتغير تحته، فأعطه نسخة بـ `toList()`، أو استخدم مكتبة `kotlinx.collections.immutable` التي تقدم أنواعاً ثابتة فعلاً. وهذا يهم كثيراً مع الروتينات، فقائمة تقرؤها في روتين ويعدلها روتين آخر هي حالة سباق كما رأيت في مقال التزامن.

### الخطأ 4: الدالة copy() نسخة سطحية Shallow Copy

```kotlin
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}")
```

```text
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 يشيران إلى نفس القائمة MutableList، وقائمة للقراءة فقط List تشير إلى نفس الكائن الذي يعدله مرجع آخر](https://arabroot.io/content/images/2026/10/kotlin-idioms-03-shared-state.webp)

القائمة للقراءة فقط و`copy()` كلاهما يشارك المرجع: من يملك مرجعاً قابلاً للتعديل يغير ما يراه الآخرون

## التحكم في التدفق Control Flow: أقواس تبدو بريئة

### الخطأ 6: return داخل forEach

القادم من Java يكتب `forEach` كأنها حلقة `for`، ويستخدم `return` ليتخطى عنصراً كما يستخدم `continue`:

```kotlin
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
}
```

```text
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](https://kotlinlang.org/docs/returns.html?ref=arabroot.io). والسطر الثالث يبين أن الرجوع غير المحلي مفيد عندما تقصده، فدالة «أول منتج نفد» تخرج عند أول نتيجة. والقاعدة: إذا احتجت `break` أو `continue` فاكتب حلقة `for` عادية، فهي أوضح للقارئ من أي علامة.

### الخطأ 7: فخ ?.let { } ?: الفرع البديل

وهذا من أشهر أخطاء القادمين من JavaScript الذين يعاملون `?.let { } ?: ...` كأنها `if/else` مختصرة. الدالة `audit` تسجل الاسم وترجع null أحياناً (هنا: للأسماء الطويلة):

```kotlin
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"
```

```text
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](https://kotlinlang.org/docs/scope-functions.html?ref=arabroot.io) متى تستخدم `let` و`also` و`apply` و`run`، وأهم سطر فيها أن `also` ترجع الكائن نفسه وليس نتيجة الـ Lambda، فـ `name?.also { audit(it) } ?: ...` لا يقع في هذا الفخ.

![مسار name?.let { audit(it) } ?: مع اسم موجود: let ترجع null من audit فيعمل الفرع البديل name is missing](https://arabroot.io/content/images/2026/10/kotlin-idioms-04-let-trap.webp)

العامل `?:` يسأل عن نتيجة السلسلة كلها، وليس عن المتغير الأول

### الخطأ 9: else على نوع sealed يخفي الحالة الجديدة

طرق الدفع نوع مغلق، والدالة `feeWrong` تحسب رسوم الدفع: البطاقة 150 هللة، وكل ما عداها صفر، فكتب المطور `else` كما يكتب `default` في `switch`. ثم أضاف فريق آخر نوعاً ثالثاً هو المحفظة الإلكترونية، ورسومها في العقد ليست صفراً:

```kotlin
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 أيضاً بالخطأ
}
```

```text
Card: fee=150
Cash: fee=0
Wallet: fee=0
```

والبناء نجح، والتشغيل نجح، والمحفظة صارت بلا رسوم دون أن يعرف أحد. ثم جربنا «ماذا يحدث إذا» كتبنا الدالة بالأسلوب الصحيح، أي كل حالة باسمها دون `else`، على نفس الكود الذي فيه `Wallet`:

```text
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](https://kotlinlang.org/docs/sealed-classes.html?ref=arabroot.io): أن يتحول نسيان حالة جديدة من خطأ مالي صامت إلى خطأ ترجمة. و`else` على نوع مغلق تلغي هذه الحماية بالكامل، فلا تكتبها إلا إذا كانت الحالات الأخرى كلها فعلاً لها نفس المعنى الآن وفي المستقبل.

### الخطأ 10: دالة امتداد Extension بنفس اسم دالة Member في الكلاس

القادم من JavaScript أو Ruby قد يظن أن دالة الامتداد Extension Function تعيد تعريف الدالة الموجودة، كما يفعل تعديل الـ Prototype. فالكلاس `Price` فيه `format()` تطبع الهللات، وأراد مطور أن يغيرها لتطبع بالريال، فكتب دالة امتداد بنفس الاسم:

```kotlin
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)
```

```text
Price(2970).format()    -> 2970 halalas
Price(2970).formatSar() -> 29.70 SAR
```

ودالة الامتداد لم تستدع أبداً، لأن القاعدة في [Extensions](https://kotlinlang.org/docs/extensions.html?ref=arabroot.io) أن دالة الكلاس نفسه Member تفوز دائماً عند تساوي الاسم والمعاملات. والمترجم حذرنا من ذلك وقت البناء بالسطر `This extension is shadowed by a member` الذي رأيته في أول المقال، وهذا سبب آخر لجعل التحذيرات أخطاء في خط CI. والإصلاح اسم مختلف كما في `formatSar`، أو تعديل الكلاس نفسه إذا كنت تملكه.

## الأرقام والنصوص Numbers and Strings

### الخطأ 8: Double للمال

وهذا خطأ عام في كل اللغات، ولكنه يتكرر في Kotlin لأن `9.90` تكتب بسهولة وتبدو كسعر:

```text
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 من الداخل](https://arabroot.io/articles/%D8%A7%D9%84%D8%A3%D8%B1%D9%82%D8%A7%D9%85-%D8%A7%D9%84%D8%B9%D8%B4%D8%B1%D9%8A%D8%A9-floating-point-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/). والسطر الثالث خادع أكثر، فالضريبة 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 أن يحسب بالهللات، وقع في الخطأ المعاكس: حساب نسبة الخصم بالأعداد الصحيحة بترتيب يبدو منطقياً على الورق، أي «اقسم على مئة ثم اضرب في النسبة»:

```kotlin
fun discountWrong(price: Int, percent: Int): Int = price / 100 * percent
fun discountFixed(price: Int, percent: Int): Int = (price * percent + 50) / 100
```

```text
price=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 يبني نص الفاتورة بـ `+=` في حلقة، وهذا شكل الفاتورة بعشرين ألف سطر:

```kotlin
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') }.length
```

```text
s += 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 قديم في نفس المشروع:

```kotlin
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)
```

```java
public class LegacyCaller {
    public static int total() {
        return MistakesKt.priceWithVat(990);
    }
}
```

```text
> 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 ترجم وأعطى:

```text
Java caller: LegacyCaller.total() = 1139
```

أي 990 هللة زائد ضريبة 149 هللة. ولاحظ اسم الكلاس `MistakesKt` في كود Java، فهو الكلاس الذي يولده المترجم لدوال المستوى الأعلى في الملف `Mistakes.kt` كما رأينا في المقال الأول من المرحلة، وتستطيع أن تغيره بالعلامة `@file:JvmName("Pricing")` حتى يقرأ كود Java بشكل طبيعي. وهذا الخطأ من النوع الجيد، لأنه خطأ ترجمة وليس نتيجة خاطئة، ولكنه يظهر متأخراً، عندما يحاول فريق آخر استخدام مكتبتك. فإذا كانت دوالك العامة ستستدعى من Java، فراجع ثلاث علامات في صفحة [Calling Kotlin from Java](https://kotlinlang.org/docs/java-to-kotlin-interop.html?ref=arabroot.io): `@JvmOverloads` للقيم الافتراضية، و`@JvmStatic` لدوال `companion object`، و`@Throws` للاستثناءات كما رأينا في المقال الثاني.

## الروتينات المساعدة Coroutines: الإلغاء والنطاق

### الخطأ 11: runCatching تبتلع الإلغاء Cancellation

القادم من Python أو JavaScript يحب أن يلف كل استدعاء شبكة بشيء يمسك الأخطاء، و`runCatching` تبدو الأداة المثالية. والروتين التالي يجلب السعر خمس مرات، وكل جلب يأخذ ثانية، ونلغيه بعد 100 جزء من الألف من الثانية:

```kotlin
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()
```

```text
  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](https://kotlinlang.org/docs/cancellation-and-timeouts.html?ref=arabroot.io) أن الإلغاء استثناء عادي في ظاهره، ولذلك أي `catch (e: Exception)` أو `runCatching` حول كود معلق تحتاج هذا السطر.

### الخطأ 12: GlobalScope وعمل يستمر بعد انتهاء الطلب

القادم من JavaScript تعود أن يطلق عملاً في الخلفية دون أن ينتظره، مثل إرسال بريد التأكيد بعد الطلب، فيكتب في Kotlin `GlobalScope.launch`:

```kotlin
@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"
}
```

```text
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` معلمة في المكتبة كواجهة حساسة، وتقول [وثائقها](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/?ref=arabroot.io) إنها لا تستخدم في كود التطبيقات عادة. والمخرج يبين لماذا: الرد ذهب للعميل والسجل فيه «request handled» فقط، والبريد أرسل بعد 200 جزء من الألف، خارج أي نطاق، فلا أحد يلغيه إذا ألغي الطلب، ولا أحد يعرف إذا فشل، وإذا أغلقت الخدمة في تلك اللحظة ضاع دون أثر. والنسخة المصلحة تطلق نفس العمل داخل `coroutineScope`، فالدالة لا ترجع حتى ينتهي البريد، وأي خطأ فيه يصل إلى المستدعي. وإذا كان المطلوب فعلاً ألا ينتظر العميل البريد، فالحل ليس `GlobalScope`، وإنما نطاق تملكه الخدمة نفسها بدورة حياتها، أو الأفضل طابور Queue خارجي يضمن الإرسال حتى لو أعيد تشغيل الخدمة.

وفي خدمة Ktor تحديداً، لكل طلب نطاق روتينات خاص به ينتهي بانتهاء الطلب، وللتطبيق كله نطاق ينتهي عند إيقاف السيرفر. فإذا أردت عملاً يستمر بعد الرد، مثل تحديث إحصائية لا تهم العميل، فأطلقه في نطاق التطبيق وليس في `GlobalScope`، فيلغى عند إيقاف الخدمة بشكل منظم بدل أن يقطع في منتصفه، وتصل أخطاؤه إلى معالج الأخطاء الذي تضبطه أنت. والفرق يبدو صغيراً في الكود، ولكنه الفرق بين عمل له صاحب يعرف متى يبدأ ومتى ينتهي، وعمل يعيش وحده حتى يموت مع العملية.

![خطآن في الروتينات: runCatching تمسك الإلغاء فتكمل الحلقة خمس جولات، وGlobalScope يرسل البريد بعد أن انتهى الطلب خارج أي نطاق](https://arabroot.io/content/images/2026/10/kotlin-idioms-05-coroutine-mistakes.webp)

الإلغاء استثناء يجب أن يمر، والعمل في الخلفية يحتاج نطاقاً يملكه أحد

## ماذا تكشف الأدوات؟ تحذيرات المترجم Compiler Warnings وأداة detekt

والسؤال الطبيعي بعد هذه القائمة: أليست هناك أداة تكشف هذه الأخطاء تلقائياً؟ وجربنا الأداتين اللتين رأيناهما في المقال الأول على نفس الملف. الأولى أن نجعل كل تحذير من المترجم خطأً، بإضافة سطر واحد إلى ملف البناء:

```kotlin
kotlin { compilerOptions { allWarningsAsErrors.set(true) } }
```

```text
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`:

```text
> 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 هي أن تقرأ الكود ثلاث مرات بثلاث عيون مختلفة، بدل قراءة واحدة تحاول فيها أن ترى كل شيء:

1. **قراءة الأنواع**: ابحث عن كل `!!` وكل `as` وكل `var` داخل `data class` وكل `MutableList` في توقيع دالة عامة، فهذه أماكن يقول فيها الكود «ثق بي» للمترجم، وكل واحدة منها تحتاج سبباً مكتوباً.
2. **قراءة التدفق**: ابحث عن `return` داخل الأقواس المعقوفة، وعن `?:` بعد سلسلة طويلة، وعن `else` في `when`، واسأل في كل مرة: ماذا يحدث في الحالة التي لم يفكر فيها الكاتب؟
3. **قراءة الروتينات**: ابحث عن `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 من أفضل وثائق اللغات المتاحة، فهي مكتوبة لمطور يعرف البرمجة، ومليئة بأمثلة تعمل في المتصفح. والترتيب التالي هو الذي نقترحه لمطور قادم من لغة أخرى، بحيث يبني كل أسبوع على ما قبله:

1. **الأسبوع الأول، الجولة الرسمية**: ابدأ بـ [Kotlin Tour](https://kotlinlang.org/docs/kotlin-tour-welcome.html?ref=arabroot.io)، وهي جولة قصيرة في الأساسيات بتمارين تعمل في المتصفح، ثم صفحة [Idioms](https://kotlinlang.org/docs/idioms.html?ref=arabroot.io) التي تجمع الأساليب الشائعة في صفحة واحدة، ثم [Coding conventions](https://kotlinlang.org/docs/coding-conventions.html?ref=arabroot.io) حتى تكتب كما يكتب الآخرون من أول يوم.
2. **الأسبوع الأول أيضاً، التمارين**: [Kotlin Koans](https://kotlinlang.org/docs/koans.html?ref=arabroot.io) مجموعة تمارين صغيرة تبدأ بكود ناقص وتطلب منك إكماله حتى تنجح الاختبارات، وهي أسرع طريقة لتعويد يدك على الصيغة.
3. **إذا كنت قادماً من Java**: سلسلة الأدلة الرسمية من Java إلى Kotlin، وأهمها [Nullability in Java and Kotlin](https://kotlinlang.org/docs/java-to-kotlin-nullability-guide.html?ref=arabroot.io) و[Collections in Java and Kotlin](https://kotlinlang.org/docs/java-to-kotlin-collections-guide.html?ref=arabroot.io) و[Strings in Java and Kotlin](https://kotlinlang.org/docs/java-to-kotlin-idioms-strings.html?ref=arabroot.io)، فهي تأخذ كود Java الذي تعرفه وتكتبه بأسلوب Kotlin سطراً بسطر.
4. **الأسبوع الثاني، الأجزاء التي تختلف**: [Null safety](https://kotlinlang.org/docs/null-safety.html?ref=arabroot.io)، ثم [Scope functions](https://kotlinlang.org/docs/scope-functions.html?ref=arabroot.io) لتعرف متى تستخدم `let` و`apply` و`also` و`run`، ثم [Collections overview](https://kotlinlang.org/docs/collections-overview.html?ref=arabroot.io) و[Returns and jumps](https://kotlinlang.org/docs/returns.html?ref=arabroot.io) التي تشرح `return@label`.
5. **الأسبوع الثالث، التزامن**: [Coroutines guide](https://kotlinlang.org/docs/coroutines-guide.html?ref=arabroot.io) كاملاً بالترتيب، وهو أطول جزء وأهمه للخدمات، ثم ارجع إلى مقال التزامن من هذه المرحلة وأعد التجارب.
6. **الأسبوع الرابع، السيرفر**: [وثائق Ktor](https://ktor.io/docs/welcome.html?ref=arabroot.io) من البداية حتى الاختبار والنشر، ثم أعد بناء خدمة الطلبات من مقال الخدمة الحقيقية دون أن تنظر إلى الكود.
7. **للمرجع دائماً**: [مرجع المكتبة الأساسية kotlin-stdlib](https://kotlinlang.org/api/core/kotlin-stdlib/?ref=arabroot.io) عندما تبحث عن دالة، و[مواصفات اللغة Kotlin Specification](https://kotlinlang.org/spec/?ref=arabroot.io) عندما تريد الجواب الدقيق لسؤال مثل «متى يعمل Smart Cast بالضبط؟»، وصفحة [Learning materials](https://kotlinlang.org/docs/learning-materials-overview.html?ref=arabroot.io) التي تجمع كل المواد الرسمية الأخرى.

## تدريب عملي

- غير `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 للمطورين](https://arabroot.io/start/#stage-lang-kotlin): الأدوات والصيغة، ثم الأنواع والذاكرة والأخطاء، ثم التزامن بالروتينات، ثم خدمة الطلبات الحقيقية، ثم هذا المقال. وهذه المقالات المنشورة تكمل المرحلة:

[![](https://arabroot.io/content/images/2026/10/choose-language-cover-cover.webp)دليلكيف تختار لغة البرمجة Programming Language لمشروعك؟نختار لغة خدمة حقيقية بأسئلة الفريق والتوظيف والمكتبات والرخصة، ثم نكتبها بأربع لغات ونقيس البناء والحجم والذاكرة والطلبات في الثانية بأنفسنا.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%84%D8%BA%D8%A9-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%84%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/)[![](https://arabroot.io/content/images/2026/10/polyglot-programming-cover-cover.webp)دليلالبرمجة بعدة لغات Polyglot: متى تفيد ومتى تعقد مشروعك؟نستدعي نفس الدالة من Python بخمس طرق، عبر FFI إلى C وGo وعبر HTTP وgRPC، ونقيس كلفة كل حد بين لغتين وتعقيد البناء وما يحدث عندما يتعطل الطرف الآخر.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A8%D8%B9%D8%AF%D8%A9-%D9%84%D8%BA%D8%A7%D8%AA-polyglot-%D9%85%D8%AA%D9%89-%D8%AA%D9%81%D9%8A%D8%AF-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%B9%D9%82%D8%AF-%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/)[![](https://arabroot.io/content/images/2026/10/paradigms-types-cover-cover.webp)دليلأنماط البرمجة وأنظمة الأنواع: لماذا تختلف اللغات؟نحل مسألة واحدة بعدة أساليب برمجة، ونرى هل تكتشف اللغة أخطاءك قبل التشغيل أم بعده، وماذا يعني ذلك عند اختيار لغة لمشروعك.](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/error-handling-cover-cover.webp)دليلكيف تتعامل لغات البرمجة مع الأخطاء؟لماذا ترمي Java وPython الاستثناءات بينما ترجع Go وRust الخطأ كقيمة، وكيف تمنع الخطأ من أن يختفي بصمت في كودك.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1/)[![](https://arabroot.io/content/images/2026/10/threading-cover-cover.webp)دليلThreads وasync/await: كود لا يتجمد ولا يضيع بياناتهكيف تكتب كوداً يعمل في أكثر من خيط Thread دون أن تضيع بياناته أو يتجمد، ولماذا تظهر هذه الأخطاء على السيرفر ولا تظهر على جهازك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-threads-%D9%88%D8%A7%D9%84%D8%A3%D9%82%D9%81%D8%A7%D9%84-%D9%88-async-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/memory-management-cover-cover.webp)دليلإدارة الذاكرة: يدوياً أم Garbage Collector أم Ownership؟نقيس بأيدينا في C وGo وPython وRust من يحرر الذاكرة ومتى، وكم يكلف ذلك من الذاكرة والوقت، وما الأخطاء التي تبقى ممكنة في كل طريقة.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%8A%D8%AF%D9%88%D9%8A%D8%A7-%D8%A7%D9%85-garbage-collector-%D8%A7%D9%85-ownership/)[![](https://arabroot.io/content/images/2026/10/debugging-basics-cover-cover.webp)دليلقراءة ال Stack Trace وتصحيح الأخطاء Debugging للمبتدئينكيف تقرأ رسالة الخطأ في Python وJavaScript سطراً بسطر، ومتى تستخدم الطباعة أو نقاط التوقف في VS Code، وكيف يجد git bisect التعديل الذي أدخل الخطأ.](https://arabroot.io/articles/%D9%82%D8%B1%D8%A7%D8%A1%D8%A9-%D8%B1%D8%B3%D8%A7%D9%84%D8%A9-%D8%A7%D9%84%D8%AE%D8%B7%D8%A7-stack-trace-%D9%88%D8%A7%D9%84%D8%AA%D8%B5%D8%AD%D9%8A%D8%AD-debugging/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتشغيل الأخطاء الخمسة عشر وإصلاحاتها على Kotlin 2.4.21 وkotlinx.coroutines 1.11.0 وOpenJDK 25.0.5 داخل Docker.