معالجة الأخطاء في Kotlin: try و catch و throw
المستخدم يكتب حروفاً مكان الرقم، والإنترنت ينقطع وسط التحميل، والملف المطلوب محذوف. التطبيق الجيد لا يُغلق فجأة في وجه المستخدم، بل يتوقع المشكلة ويتصرف. في هذا الدرس ستتعلم نظام الاستثناءات (Exceptions) في Kotlin: كيف تلتقط الخطأ وكيف ترميه، وما الأدوات التي تجعل ذلك أقصر وأنظف من Java.
ما هو الاستثناء؟
Section titled “ما هو الاستثناء؟”عندما يحدث خطأ وقت التشغيل، تنشئ Kotlin كائناً يصف المشكلة و«ترميه». إذا لم يلتقطه أحد يتوقف البرنامج ويطبع رسالة مثل:
Exception in thread "main" java.lang.ArithmeticException: / by zero at MainKt.main(Main.kt:2)في الرسالة ثلاث معلومات ثمينة: نوع الخطأ، ووصفه، ورقم السطر. اقرأها دائماً قبل أي شيء.
try و catch
Section titled “try و catch”ضع الكود الخطِر داخل try، وخطة الطوارئ داخل catch:
try { val result = 10 / 0 println("لن يُطبع هذا السطر")} catch (e: ArithmeticException) { println("خطأ حسابي: ${e.message}")}println("البرنامج مستمر")خطأ حسابي: / by zeroالبرنامج مستمرعند حدوث الخطأ يقفز التنفيذ فوراً إلى catch وتُهمل بقية أسطر try، ثم يكمل البرنامج طريقه. المتغير e هو كائن الاستثناء، و e.message وصف المشكلة.
الأمر مثل الإطار الاحتياطي في السيارة: لا تتمنى استخدامه، لكن وجوده يجعلك تكمل الرحلة.
أكثر من catch، ثم finally
Section titled “أكثر من catch، ثم finally”val inputs = listOf("25", "abc", "0")for (input in inputs) { try { val number = input.toInt() println("100 / $number = ${100 / number}") } catch (e: NumberFormatException) { println("«$input» ليس رقماً") } catch (e: ArithmeticException) { println("لا يمكن القسمة على صفر") } finally { println("-- انتهت معالجة $input") }}100 / 25 = 4-- انتهت معالجة 25«abc» ليس رقماً-- انتهت معالجة abcلا يمكن القسمة على صفر-- انتهت معالجة 0- Kotlin تجرّب أقسام
catchبالترتيب وتنفّذ أول قسم يطابق نوع الخطأ. finallyيُنفَّذ دائماً: سواء نجح الكود أو فشل. نضع فيه أعمال التنظيف مثل إغلاق ملف أو إخفاء مؤشر التحميل.
try تعبير يُرجع قيمة
Section titled “try تعبير يُرجع قيمة”مثل if و when، الكلمة try في Kotlin تعبير: قيمتها هي آخر سطر في القسم الذي نُفِّذ:
fun parseAge(text: String): Int { val age = try { text.toInt() } catch (e: NumberFormatException) { -1 } return age}println(parseAge("15"))println(parseAge("خمسة عشر"))15-1بهذا تعرّف المتغير بـ val وتعطيه قيمته في خطوة واحدة، بدل تعريف var فارغ ثم ملئه.
throw: ارمِ استثناءك
Section titled “throw: ارمِ استثناءك”أنت أيضاً ترمي استثناءً عندما تستقبل دالتك قيمة غير منطقية:
fun withdraw(balance: Double, amount: Double): Double { if (amount > balance) { throw IllegalArgumentException("الرصيد لا يكفي لسحب $amount") } return balance - amount}try { println(withdraw(100.0, 30.0)) println(withdraw(100.0, 500.0)) println("لن نصل إلى هنا")} catch (e: IllegalArgumentException) { println("مرفوض: ${e.message}")}70.0مرفوض: الرصيد لا يكفي لسحب 500.0throw توقف الدالة فوراً وترسل الخطأ إلى من استدعاها. ولاحظ أن Kotlin لا تحتاج الكلمة new.
require و check: التحقق المختصر
Section titled “require و check: التحقق المختصر”كتابة if ثم throw تتكرر كثيراً، فوفرت Kotlin دالتين جاهزتين:
class Timer(val seconds: Int) { var running = false
init { require(seconds > 0) { "المدة يجب أن تكون أكبر من صفر" } }
fun stop() { check(running) { "المؤقت متوقف أصلاً" } running = false }}try { Timer(0)} catch (e: IllegalArgumentException) { println(e.message)}
try { Timer(30).stop()} catch (e: IllegalStateException) { println(e.message)}المدة يجب أن تكون أكبر من صفرالمؤقت متوقف أصلاًrequire(شرط): للتحقق من المعاملات القادمة للدالة. ترميIllegalArgumentException.check(شرط): للتحقق من حالة الكائن نفسه. ترميIllegalStateException.
استثناء خاص بك
Section titled “استثناء خاص بك”للأخطاء الخاصة بتطبيقك، أنشئ كلاساً يرث من Exception:
class OutOfStockException(val product: String) : Exception("المنتج «$product» غير متوفر")
fun order(product: String, stock: Map<String, Int>) { val available = stock[product] ?: 0 if (available == 0) throw OutOfStockException(product) println("تم طلب $product")}val stock = mapOf("قلم" to 5, "دفتر" to 0)try { order("قلم", stock) order("دفتر", stock)} catch (e: OutOfStockException) { println(e.message) println("المنتج: ${e.product}")}تم طلب قلمالمنتج «دفتر» غير متوفرالمنتج: دفترالاسم الواضح للاستثناء يشرح المشكلة، ويمكنه أن يحمل بيانات إضافية مثل product.
runCatching: أسلوب بديل
Section titled “runCatching: أسلوب بديل”الدالة runCatching تنفّذ الكود وتضع النتيجة في كائن Result: إما قيمة ناجحة أو خطأ:
val good = runCatching { "42".toInt() }val bad = runCatching { "abc".toInt() }
println(good.isSuccess)println(good.getOrDefault(0))println(bad.isFailure)println(bad.getOrDefault(0))true42true0وهي مفيدة عندما تريد قيمة بديلة في سطر واحد دون كتابة try/catch كاملة.
جرّب بنفسك
Section titled “جرّب بنفسك”- اكتب دالة
divide(a: Int, b: Int): Intتستخدمrequireلرفض القسمة على صفر برسالة عربية. - استدعها داخل
try/catchمرتين: بقيم سليمة ثم بصفر، وأضفfinally. - اكتب دالة
safeDivideتستخدمtryكتعبير وتُرجعnullعند الخطأ. - تحدٍّ: أنشئ
class InvalidAgeException، ودالةparseAge(text: String): Intترميه إذا كان النص ليس رقماً أو كان العمر خارج المدى من 0 إلى 120.
أخطاء شائعة
Section titled “أخطاء شائعة”- catch فارغ: كتابة
catch (e: Exception) {}تبتلع الخطأ بصمت، فتبحث ساعات عن مشكلة لا أثر لها. اطبع رسالة على الأقل. - التقاط Exception العامة دائماً: تخفي أخطاء لم تتوقعها. التقط النوع المحدد الذي تعرف كيف تعالجه.
- الاستثناءات بدل الأدوات الآمنة: لا تكتب
try/catchحولtoInt()إذا كانتtoIntOrNull()تكفي، ولا حولmap[key]!!إذا كان المعامل?:يكفي. - ترتيب catch: ضع الأنواع الأخص أولاً. لو بدأت بـ
Exceptionفلن يصل التنفيذ إلى ما بعدها أبداً.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
متى يُنفَّذ الكود داخل finally؟
finally يعمل في الحالتين، ولذلك نضع فيه أعمال التنظيف.
سؤال
أي استثناء ترميه require(age >= 0) إذا كان الشرط خاطئاً؟
require تفحص المعاملات وترمي IllegalArgumentException، أما check فترمي IllegalStateException.
الخلاصة
Section titled “الخلاصة”tryللكود الخطِر، وcatchلخطة الطوارئ، وfinallyلما يُنفَّذ دائماً.tryتعبير يُرجع قيمة، فيمكن إسناده إلىvalمباشرة.throwترمي استثناءً، وrequireوcheckتختصران التحقق.- أنشئ استثناءك الخاص بوراثة
Exception، ويمكنه حمل بيانات إضافية. - كل استثناءات Kotlin غير مفحوصة، و
runCatchingبديل مختصر.