معالجة الأخطاء في Kotlin: try/catch والتقنيات المتقدمة

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: لماذا لا يكفي try/catch القياسي؟

\n\n

التطوير باستخدام Kotlin، خاصة في سياق تطبيقات الخادم أو Android، غالباً ما يواجه الحاجة إلى معالجة موثوقة للأخطاء. كتلة try/catch القياسية هي الأساس، لكنها عملياً تؤدي إلى "تشتت" المنطق وتعقيد الاختبار. على المستوى المتقدم، لا نريد فقط التقاط الاستثناءات، بل تصميم النظام بحيث تكون الأخطاء جزءاً من نوع البيانات، وليس تدفق تحكم غير متوقع.

\n\n

في هذه المقالة، سنستعرض كيفية تجاوز try/catch واستخدام الأساليب الوظيفية: Result و Either (من المكتبات) و sealed class. ستتعلم كيفية جعل الكود قابلاً للتنبؤ، آمناً من حيث الأنواع، وسهل الصيانة.

\n\n

1. try/catch التقليدي: متى لا يزال مفيداً؟

\n\n

لا ينبغي التخلي تماماً عن try/catch. فهو لا غنى عنه للعمل مع الكود القديم (legacy)، والمكتبات الخارجية (مثل تحليل JSON)، واستدعاءات النظام (الإدخال/الإخراج). ومع ذلك، هناك فروق دقيقة مهمة.

\n\n

الهيكل الصحيح والموارد

\n

في Kotlin، على عكس Java، لا توجد استثناءات مُتحقق منها (checked exceptions)، ولكن هناك بنية أنيقة use للإغلاق التلقائي للموارد:

\n\n
fun readFile(path: String): String? = try {\n    BufferedReader(FileReader(path)).use { it.readText() }\n} catch (e: IOException) {\n    println("خطأ في قراءة الملف: ${e.message}")\n    null\n}
\n\n

هنا، use يضمن إغلاق الملف، حتى في حالة حدوث استثناء داخلي. هذا بديل أنيق لـ try-with-resources.

\n\n

المشكلة: فقدان السياق

\n

العيب الرئيسي لـ try/catch هو أنه لا يُعلم الكود المستدعي أن الدالة قد "تفشل". الاستثناء هو تأثير جانبي مخفي. استدعاء readFile() قد يُرجع null أو يلقي IOException، لكن توقيع الدالة لا يعكس ذلك. هذا يؤدي إلى أخطاء في وقت التشغيل.

\n\n

الحل هو الانتقال إلى تمثيل صريح للأخطاء.

\n\n

2. Result: أحادية (Monad) مدمجة للنجاح أو الفشل

\n\n

توفر Kotlin فئة قياسية Result<T>. وهي بنية جبرية بسيطة: إما نجاح (قيمة T) أو فشل (Throwable). هذا يجعل معالجة الأخطاء جزءاً من نظام الأنواع.

\n\n

الاستخدام الأساسي

\n
fun riskyDivision(a: Int, b: Int): Result<Int> = runCatching {\n    a / b\n}\n\nfun main() {\n    val result = riskyDivision(10, 0)\n    result.onSuccess { println("النتيجة: $it") }\n         .onFailure { println("خطأ: ${it.message}") }\n}
\n\n

runCatching هو غلاف لـ try/catch يُرجع Result. تسمح الطريقتان onSuccess و onFailure بالتفاعل مع النتيجة دون مقاطعة سلسلة الاستدعاءات.

\n\n

التحويل والسلاسل

\n

يمكنك تحويل النتيجة دون القلق بشأن الاستثناءات داخل map:

\n\n
val finalResult = riskyDivision(10, 2)\n    .map { it * 2 }          // إذا كان نجاحاً - نضرب\n    .recover { error ->      // إذا كان خطأ - نعيد قيمة احتياطية\n        println("نستخدم قيمة احتياطية بسبب: ${error.message}")\n        0\n    }\nprintln(finalResult.getOrDefault(-1)) // 10
\n\n

هام: Result غير مخصص للتخزين في المجموعات أو النقل بين الخيوط دون معالجة إضافية. لهذه الأغراض، من الأفضل استخدام sealed class أو مكتبات مثل Arrow.

\n\n

3. Either: التمييز بين أنواع الأخطاء

\n\n

فئة Result تلتقط فقط Throwable. لكننا غالباً ما نريد التمييز بين أنواع الأخطاء: "غير موجود"، "صلاحيات غير كافية"، "خطأ في التحقق". هنا تأتي Either<L, R> من المكتبات الوظيفية (مثل Arrow أو المكتوبة يدوياً

المدونات

توصيات الكتب