تخطَّ إلى المحتوى

معالجة الأخطاء في Kotlin: try و catch و throw

الدرس 10 من 11 · Kotlin

المستخدم يكتب حروفاً مكان الرقم، والإنترنت ينقطع وسط التحميل، والملف المطلوب محذوف. التطبيق الجيد لا يُغلق فجأة في وجه المستخدم، بل يتوقع المشكلة ويتصرف. في هذا الدرس ستتعلم نظام الاستثناءات (Exceptions) في Kotlin: كيف تلتقط الخطأ وكيف ترميه، وما الأدوات التي تجعل ذلك أقصر وأنظف من Java.

عندما يحدث خطأ وقت التشغيل، تنشئ Kotlin كائناً يصف المشكلة و«ترميه». إذا لم يلتقطه أحد يتوقف البرنامج ويطبع رسالة مثل:

Exception in thread "main" java.lang.ArithmeticException: / by zero
at MainKt.main(Main.kt:2)

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

ضع الكود الخطِر داخل try، وخطة الطوارئ داخل catch:

try {
val result = 10 / 0
println("لن يُطبع هذا السطر")
} catch (e: ArithmeticException) {
println("خطأ حسابي: ${e.message}")
}
println("البرنامج مستمر")
الناتج
خطأ حسابي: / by zero
البرنامج مستمر

عند حدوث الخطأ يقفز التنفيذ فوراً إلى catch وتُهمل بقية أسطر try، ثم يكمل البرنامج طريقه. المتغير e هو كائن الاستثناء، و e.message وصف المشكلة.

الأمر مثل الإطار الاحتياطي في السيارة: لا تتمنى استخدامه، لكن وجوده يجعلك تكمل الرحلة.

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 يُنفَّذ دائماً: سواء نجح الكود أو فشل. نضع فيه أعمال التنظيف مثل إغلاق ملف أو إخفاء مؤشر التحميل.

مثل 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 فارغ ثم ملئه.

أنت أيضاً ترمي استثناءً عندما تستقبل دالتك قيمة غير منطقية:

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.0

throw توقف الدالة فوراً وترسل الخطأ إلى من استدعاها. ولاحظ أن 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.

للأخطاء الخاصة بتطبيقك، أنشئ كلاساً يرث من 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 تنفّذ الكود وتضع النتيجة في كائن 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))
الناتج
true
42
true
0

وهي مفيدة عندما تريد قيمة بديلة في سطر واحد دون كتابة try/catch كاملة.

  1. اكتب دالة divide(a: Int, b: Int): Int تستخدم require لرفض القسمة على صفر برسالة عربية.
  2. استدعها داخل try/catch مرتين: بقيم سليمة ثم بصفر، وأضف finally.
  3. اكتب دالة safeDivide تستخدم try كتعبير وتُرجع null عند الخطأ.
  4. تحدٍّ: أنشئ class InvalidAgeException، ودالة parseAge(text: String): Int ترميه إذا كان النص ليس رقماً أو كان العمر خارج المدى من 0 إلى 120.
  • catch فارغ: كتابة catch (e: Exception) {} تبتلع الخطأ بصمت، فتبحث ساعات عن مشكلة لا أثر لها. اطبع رسالة على الأقل.
  • التقاط Exception العامة دائماً: تخفي أخطاء لم تتوقعها. التقط النوع المحدد الذي تعرف كيف تعالجه.
  • الاستثناءات بدل الأدوات الآمنة: لا تكتب try/catch حول toInt() إذا كانت toIntOrNull() تكفي، ولا حول map[key]!! إذا كان المعامل ?: يكفي.
  • ترتيب catch: ضع الأنواع الأخص أولاً. لو بدأت بـ Exception فلن يصل التنفيذ إلى ما بعدها أبداً.

سؤال

متى يُنفَّذ الكود داخل finally؟

سؤال

أي استثناء ترميه require(age >= 0) إذا كان الشرط خاطئاً؟

  • try للكود الخطِر، و catch لخطة الطوارئ، و finally لما يُنفَّذ دائماً.
  • try تعبير يُرجع قيمة، فيمكن إسناده إلى val مباشرة.
  • throw ترمي استثناءً، و require و check تختصران التحقق.
  • أنشئ استثناءك الخاص بوراثة Exception، ويمكنه حمل بيانات إضافية.
  • كل استثناءات Kotlin غير مفحوصة، و runCatching بديل مختصر.