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

معالجة الأخطاء في Swift: throws و do و try و catch

الدرس 10 من 11 · Swift

الإنترنت ينقطع وسط التحميل، والملف المطلوب محذوف، والمستخدم يطلب سحب مبلغ أكبر من رصيده. التطبيق الجيد لا ينهار أمام هذه المواقف، بل يتوقعها ويتصرف. تعلمت من قبل أن الاختياريات تعبّر عن «قيمة قد تغيب». في هذا الدرس ستتعلم الأداة الأقوى: نظام الأخطاء في Swift، الذي يخبرك لماذا فشلت العملية وليس فقط أنها فشلت.

أول خطوة أن تصف الأخطاء المحتملة. نستخدم لذلك التعداد (enum): نوع قيمه قائمة محددة من الحالات، ونجعله يطابق البروتوكول Error:

enum BankError: Error {
case invalidAmount
case insufficientFunds(missing: Double)
}
  • case invalidAmount: حالة بسيطة: المبلغ غير صالح.
  • case insufficientFunds(missing: Double): حالة تحمل بيانات إضافية: كم ينقص المستخدم.

التعداد مثل قائمة الأعطال في دليل السيارة: كل عطل له اسم واضح، فتعرف ما المشكلة بالضبط.

الدالة التي قد تفشل تُعلن ذلك بكلمة throws في توقيعها، وترمي الخطأ بـ throw:

func withdraw(_ amount: Double, from balance: Double) throws -> Double {
guard amount > 0 else {
throw BankError.invalidAmount
}
guard amount <= balance else {
throw BankError.insufficientFunds(missing: amount - balance)
}
return balance - amount
}
  • throws قبل السهم: إعلان صريح بأن الدالة قد ترمي خطأ.
  • guard ... else: «تأكد من الشرط، وإلا اخرج». مثالية للتحقق في أول الدالة.
  • throw توقف الدالة فوراً وترسل الخطأ إلى من استدعاها.

لا تسمح Swift باستدعاء دالة throws كأي دالة عادية. يجب أن تكتب try قبلها، وأن تضعها داخل do مع catch:

do {
let rest = try withdraw(30, from: 100)
print("الباقي: \(rest)")
let rest2 = try withdraw(500, from: 100)
print("الباقي: \(rest2)")
} catch BankError.insufficientFunds(let missing) {
print("الرصيد لا يكفي، ينقصك \(missing)")
} catch BankError.invalidAmount {
print("المبلغ غير صالح")
} catch {
print("خطأ غير متوقع: \(error)")
}
print("التطبيق مستمر")
الناتج
الباقي: 70.0
الرصيد لا يكفي، ينقصك 400.0
التطبيق مستمر
  • try: علامة ظاهرة على كل سطر قد يفشل، فتعرف مواضع الخطر بنظرة واحدة.
  • عند رمي الخطأ يقفز التنفيذ إلى أول catch مطابق، وتُهمل بقية أسطر do.
  • catch BankError.insufficientFunds(let missing): يلتقط هذه الحالة تحديداً ويستخرج بياناتها.
  • catch الأخيرة بلا نوع تلتقط أي خطأ آخر، وتجد الخطأ جاهزاً في ثابت اسمه error.

الأمر مثل شبكة الأمان تحت لاعب السيرك: إذا سقط التقطته الشبكة واستمر العرض.

إذا لم تعرف دالتك كيف تعالج الخطأ، فلا تلتقطه. أعلن throws هي الأخرى ودعه يصعد إلى من يستدعيها:

func buy(price: Double, balance: Double) throws -> Double {
let rest = try withdraw(price, from: balance)
print("تم الشراء")
return rest
}
do {
let rest = try buy(price: 80, balance: 100)
print(rest)
_ = try buy(price: -5, balance: 100)
} catch {
print("فشلت العملية: \(error)")
}
الناتج
تم الشراء
20.0
فشلت العملية: invalidAmount

buy لم تكتب do/catch، فانتقل الخطأ عبرها إلى من استدعاها. والقاعدة: التقط الخطأ في المكان الذي تستطيع أن تفعل فيه شيئاً مفيداً، كعرض رسالة للمستخدم.

أحياناً لا يهمك سبب الفشل، وتريد النتيجة أو لا شيء. try? تحوّل الناتج إلى قيمة اختيارية:

let good = try? withdraw(40, from: 100)
let bad = try? withdraw(900, from: 100)
print(good ?? -1)
print(bad ?? -1)
if let rest = try? withdraw(10, from: 50) {
print("نجح السحب، الباقي \(rest)")
}
الناتج
60.0
-1.0
نجح السحب، الباقي 40.0

أما try! فتقول للمترجم: «أنا متأكد أنها لن تفشل». وإذا فشلت ينهار التطبيق فوراً. تجنبها إلا في حالات نادرة تضمن فيها النجاح فعلاً.

الصيغة عند الفشل استخدمها عندما
try داخل do/catch تلتقط الخطأ وتعرف سببه تريد معالجة مختلفة لكل خطأ
try? تحصل على nil لا يهمك السبب
try! ينهار التطبيق متأكد تماماً من النجاح

الكود داخل defer يُنفَّذ عند الخروج من النطاق مهما كان السبب: نهاية طبيعية، أو return، أو خطأ مرمي. مناسب لأعمال التنظيف مثل إغلاق ملف أو إخفاء مؤشر التحميل:

enum LoadError: Error {
case noConnection
}
func loadProfile(online: Bool) throws {
print("إظهار مؤشر التحميل")
defer {
print("إخفاء مؤشر التحميل")
}
guard online else {
throw LoadError.noConnection
}
print("تم تحميل الملف الشخصي")
}
do {
try loadProfile(online: true)
try loadProfile(online: false)
} catch {
print("تعذّر التحميل: \(error)")
}
الناتج
إظهار مؤشر التحميل
تم تحميل الملف الشخصي
إخفاء مؤشر التحميل
إظهار مؤشر التحميل
إخفاء مؤشر التحميل
تعذّر التحميل: noConnection

في المرتين اختفى المؤشر، حتى عندما خرجت الدالة بخطأ. وهذا دور finally في اللغات الأخرى.

ليس كل ما يوقف التطبيق خطأً يمكن التقاطه بـ catch. هذه الحالات تُنهي التطبيق فوراً ولا يلتقطها شيء:

  • الوصول إلى عنصر خارج حدود المصفوفة: numbers[10].
  • الفتح القسري ! لقيمة هي nil.
  • قسمة عدد صحيح على صفر.

الحل معها الوقاية لا العلاج: افحص بـ if و guard، واستخدم first و if let و ?? كما تعلمت. نظام throws للأخطاء المتوقعة القادمة من العالم الخارجي: شبكة، وملفات، وإدخال المستخدم.

  1. عرّف enum AgeError: Error بحالتين: notANumber و outOfRange.
  2. اكتب دالة parseAge(_ text: String) throws -> Int ترمي الخطأ المناسب إذا كان النص ليس رقماً أو كان العمر خارج المدى من 0 إلى 120.
  3. استدعها داخل do/catch مع ثلاثة نصوص مختلفة، واطبع رسالة عربية لكل خطأ.
  4. تحدٍّ: استدعِ الدالة نفسها بـ try? مع ?? لتحصل على عمر افتراضي 18 عند الفشل.
  • نسيان try: استدعاء دالة throws بدونها يعطي: Call can throw but is not marked with 'try'.
  • catch واحدة عامة لكل شيء: تخفي نوع المشكلة عن المستخدم. التقط الحالات التي تعرفها أولاً، ثم العامة في النهاية.
  • الإفراط في try!: كل try! نقطة انهيار محتملة. استخدم try? أو do/catch.
  • الخلط بين الأخطاء والاختياريات: إذا كان للفشل سبب واحد واضح (مثل «العنصر غير موجود») فالقيمة الاختيارية أبسط. استخدم throws حين تتعدد الأسباب.

سؤال

ماذا تُرجع try? riskyFunction() إذا رمت الدالة خطأ؟

سؤال

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

  • عرّف أخطاءك بـ enum يطابق Error، وحالاته قد تحمل بيانات.
  • الدالة التي قد تفشل تُعلن throws وترمي بـ throw، و guard تختصر التحقق.
  • الاستدعاء بـ try داخل do، والالتقاط بـ catch لكل حالة.
  • try? تعطي قيمة اختيارية، و try! تنهار عند الفشل فتجنبها.
  • defer للتنظيف عند الخروج، والأخطاء القاتلة مثل تجاوز حدود المصفوفة لا تُلتقط.