معالجة الأخطاء في Swift: throws و do و try و catch
الإنترنت ينقطع وسط التحميل، والملف المطلوب محذوف، والمستخدم يطلب سحب مبلغ أكبر من رصيده. التطبيق الجيد لا ينهار أمام هذه المواقف، بل يتوقعها ويتصرف. تعلمت من قبل أن الاختياريات تعبّر عن «قيمة قد تغيب». في هذا الدرس ستتعلم الأداة الأقوى: نظام الأخطاء في Swift، الذي يخبرك لماذا فشلت العملية وليس فقط أنها فشلت.
تعريف الأخطاء
Section titled “تعريف الأخطاء”أول خطوة أن تصف الأخطاء المحتملة. نستخدم لذلك التعداد (enum): نوع قيمه قائمة محددة من الحالات، ونجعله يطابق البروتوكول Error:
enum BankError: Error { case invalidAmount case insufficientFunds(missing: Double)}case invalidAmount: حالة بسيطة: المبلغ غير صالح.case insufficientFunds(missing: Double): حالة تحمل بيانات إضافية: كم ينقص المستخدم.
التعداد مثل قائمة الأعطال في دليل السيارة: كل عطل له اسم واضح، فتعرف ما المشكلة بالضبط.
throws و throw
Section titled “throws و throw”الدالة التي قد تفشل تُعلن ذلك بكلمة 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توقف الدالة فوراً وترسل الخطأ إلى من استدعاها.
do و try و catch
Section titled “do و try و catch”لا تسمح 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.
الأمر مثل شبكة الأمان تحت لاعب السيرك: إذا سقط التقطته الشبكة واستمر العرض.
تمرير الخطأ للأعلى
Section titled “تمرير الخطأ للأعلى”إذا لم تعرف دالتك كيف تعالج الخطأ، فلا تلتقطه. أعلن 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فشلت العملية: invalidAmountbuy لم تكتب do/catch، فانتقل الخطأ عبرها إلى من استدعاها. والقاعدة: التقط الخطأ في المكان الذي تستطيع أن تفعل فيه شيئاً مفيداً، كعرض رسالة للمستخدم.
try? و try!
Section titled “try? و try!”أحياناً لا يهمك سبب الفشل، وتريد النتيجة أو لا شيء. 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: نفّذ عند الخروج
Section titled “defer: نفّذ عند الخروج”الكود داخل 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 في اللغات الأخرى.
أخطاء لا تُلتقط
Section titled “أخطاء لا تُلتقط”ليس كل ما يوقف التطبيق خطأً يمكن التقاطه بـ catch. هذه الحالات تُنهي التطبيق فوراً ولا يلتقطها شيء:
- الوصول إلى عنصر خارج حدود المصفوفة:
numbers[10]. - الفتح القسري
!لقيمة هيnil. - قسمة عدد صحيح على صفر.
الحل معها الوقاية لا العلاج: افحص بـ if و guard، واستخدم first و if let و ?? كما تعلمت. نظام throws للأخطاء المتوقعة القادمة من العالم الخارجي: شبكة، وملفات، وإدخال المستخدم.
جرّب بنفسك
Section titled “جرّب بنفسك”- عرّف
enum AgeError: Errorبحالتين:notANumberوoutOfRange. - اكتب دالة
parseAge(_ text: String) throws -> Intترمي الخطأ المناسب إذا كان النص ليس رقماً أو كان العمر خارج المدى من 0 إلى 120. - استدعها داخل
do/catchمع ثلاثة نصوص مختلفة، واطبع رسالة عربية لكل خطأ. - تحدٍّ: استدعِ الدالة نفسها بـ
try?مع??لتحصل على عمر افتراضي 18 عند الفشل.
أخطاء شائعة
Section titled “أخطاء شائعة”- نسيان try: استدعاء دالة
throwsبدونها يعطي:Call can throw but is not marked with 'try'. - catch واحدة عامة لكل شيء: تخفي نوع المشكلة عن المستخدم. التقط الحالات التي تعرفها أولاً، ثم العامة في النهاية.
- الإفراط في try!: كل
try!نقطة انهيار محتملة. استخدمtry?أوdo/catch. - الخلط بين الأخطاء والاختياريات: إذا كان للفشل سبب واحد واضح (مثل «العنصر غير موجود») فالقيمة الاختيارية أبسط. استخدم
throwsحين تتعدد الأسباب.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ماذا تُرجع try? riskyFunction() إذا رمت الدالة خطأ؟
try? تحوّل الناتج إلى قيمة اختيارية: القيمة عند النجاح، و nil عند أي خطأ.
سؤال
متى يُنفَّذ الكود داخل defer؟
defer يؤجل التنفيذ إلى لحظة الخروج من النطاق مهما كان سبب الخروج.
الخلاصة
Section titled “الخلاصة”- عرّف أخطاءك بـ
enumيطابقError، وحالاته قد تحمل بيانات. - الدالة التي قد تفشل تُعلن
throwsوترمي بـthrow، وguardتختصر التحقق. - الاستدعاء بـ
tryداخلdo، والالتقاط بـcatchلكل حالة. try?تعطي قيمة اختيارية، وtry!تنهار عند الفشل فتجنبها.deferللتنظيف عند الخروج، والأخطاء القاتلة مثل تجاوز حدود المصفوفة لا تُلتقط.