EduFlow
أساسيات Swift لتطبيقات iOS
⁦0%⁩

الوحدة 2 · تصميم البيانات بالأنواع

معالجة الأخطاء والـ typed throws

18 دقراءة 4 داختر بحكمة

الدرس في لمحة

صِف حالات الفشل كـ enums للأخطاء، وارمِها ومرّرها باستخدام try، وعالجها بـ do-catch، وقرّر متى تستحق الـ typed throws في Swift 6 أن تستخدمها.

  1. 1الأخطاء enums
  2. 2throw وtry
  3. 3معالجة الأخطاء بـ do-catch
  4. 4try? وtry!
  5. 5رسائل يفهمها المستخدم
  6. 6الـ typed throws في Swift 6

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

ماذا يجب أن يحدث حين يحاول المستخدم حفظ مصروف بلا عنوان، أو بمبلغ سالب، أو بمبلغ يتجاوز ميزانية الطعام لهذا الشهر دفعة واحدة؟ إعادة optional، كما فعلت في درس الـ optionals، تخبر المستدعي أن العملية فشلت لكنها لا تخبره لماذا، وPocket Budget يحتاج إلى السبب ليعرض الرسالة المناسبة. معالجة الأخطاء في Swift تجعل الفشل جزءًا من توقيع الدالة، مع أسباب مكتوبة كأنواع.

الأخطاء enums

ابدأ بسرد الطرق التي قد يفشل بها تسجيل مصروف:

BudgetError.swift
import Foundation

enum BudgetError: Error, Equatable {
    case emptyTitle
    case nonPositiveAmount
    case overLimit(by: Decimal)
}

أي نوع يتوافق مع Error يمكن رميه، والـ enum ذو القيم المرتبطة هو الخيار الطبيعي: كل حالة فشلٌ واحد، وoverLimit تحمل مقدار التجاوز الذي سيقع فيه المستخدم. ليس لـ Error أي متطلّبات؛ هو فقط يعلّم النوع على أنه قابل للرمي. وسيساعد Equatable في الاختبارات في الدرس الأخير.

throw وtry

الدالة التي قد تفشل تصرّح بذلك عبر throws وتتوقّف عبر throw:

CategoryBudget.swift
struct CategoryBudget {
    var limit: Decimal
    private(set) var spent: Decimal = 0

    mutating func record(title: String, amount: Decimal) throws {
        guard !title.trimmingCharacters(in: .whitespaces).isEmpty else {
            throw BudgetError.emptyTitle
        }
        guard amount > 0 else {
            throw BudgetError.nonPositiveAmount
        }
        let newTotal = spent + amount
        guard newTotal <= limit else {
            throw BudgetError.overLimit(by: newTotal - limit)
        }
        spent = newTotal
    }
}

guard وthrow ثنائي مثالي: كل guard تنصّ على قاعدة، وفرع else فيها يرمي الخطأ المناسب. وprivate(set) تسمح لأي أحد بقراءة spent لكنها لا تسمح بتغييرها إلا للميزانية نفسها، فتكون الطريقة الوحيدة لإنفاق المال عبر record التي تفرض القواعد.

كل استدعاء لدالة ترمي أخطاء يجب أن يُعلَّم بـ try. هذه الكلمة موجّهة للبشر: امسح الدالة بعينيك بحثًا عن try وسترى كل سطر قد يغادر فيه التنفيذ مبكرًا.

معالجة الأخطاء بـ do-catch

Swift
var food = CategoryBudget(limit: 50)

do {
    try food.record(title: "Dinner", amount: 62)
    print("Saved")
} catch BudgetError.overLimit(let by) {
    print("That's \(by) over your food budget")
} catch {
    print("Couldn't save: \(error)")
}

حين ترمي record خطأً، يقفز التنفيذ مباشرة إلى أول catch يطابق نمطها، متخطّيًا print("Saved"). وتستخدم جمل catch مطابقة الأنماط نفسها التي في switch، فيمكنك ربط القيم المرتبطة. أما catch الأخيرة المجرّدة فتعالج كل ما تبقّى وتعطيك الخطأ كثابت اسمه error.

إن لم تكن الدالة قادرة على معالجة الخطأ بشكل معقول، فليس عليها ذلك. علّمها بـ throws أيضًا، واستدعِ بـ try دون do، وسيصعد الخطأ إلى من استدعاها.

ترمي record الخطأ overLimit. والدالة saveExpense معلّمة بـ throws فتمرّر الخطأ إلى الأعلى. ومعالج زر Save فيه do-catch يلتقطه ويعرض رسالة. record(...) throw .overLimit saveExpense() throws: يمرّره للأعلى Save button do-catch: يعرض تنبيهًا
يصعد الخطأ حتى يلتقطه شيء ما

try? وtry!

هناك اختصاران. try? تحوّل النتيجة إلى optional: النجاح يعطي قيمة، والفشل يعطي nil. أما try! فتؤكّد أن الاستدعاء لا يمكن أن يفشل، وتُسقط التطبيق إن فشل، بالمخاطرة نفسها التي في الفكّ القسري.

رسائل يفهمها المستخدم

print(error) تعرض overLimit(by: 12)، وهذا مقبول في السجلّ لكنه بلا فائدة في تنبيه. اجعل النوع يتوافق مع LocalizedError لتقدّم نصًا يفهمه البشر:

Swift
extension BudgetError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .emptyTitle: "Give the expense a name."
        case .nonPositiveAmount: "Enter an amount greater than zero."
        case .overLimit(let by): "This puts you \(by.formatted(.currency(code: "USD"))) over budget."
        }
    }
}

الآن تعيد error.localizedDescription تلك الجملة، جاهزة لتنبيه في SwiftUI.

الـ typed throws في Swift 6

throws العادية تعني «قد ترمي أي Error». وتضيف Swift 6 الـ typed throws (الرمي محدّد النوع)، حيث يسمّي التوقيع النوع الدقيق:

Swift
mutating func record(title: String, amount: Decimal) throws(BudgetError) {
    // same body; inside, `throw .emptyTitle` can drop the type name
}

الفائدة تظهر في موضع الاستدعاء. حين ترمي كل try داخل كتلة do الخطأ BudgetError، يكون error داخل catch من النوع BudgetError لا any Error، فيمكنك كتابة switch شاملة عليه دون حالة التقاط عامة. اكتب do throws(BudgetError) لتصرّح بذلك صراحةً:

Swift
do throws(BudgetError) {
    try food.record(title: "Snack", amount: 100)
} catch {
    switch error {
    case .emptyTitle: print("Give it a name")
    case .nonPositiveAmount: print("Amount must be positive")
    case .overLimit(let by): print("That's \(by) over budget")
    }
}

الثمن هو الجمود: متى وعدت الدالة بـ BudgetError، لا تستطيع أن تبدأ برمي خطأ فكّ ترميز أو خطأ شبكة دون تغيير توقيعها وكل من يستدعيها. وتوصية مشروع Swift نفسه هي إبقاء throws غير المحدّدة النوع خيارًا افتراضيًا، واستخدام الـ typed throws داخل الـ module حيث يعالج المستدعون كل حالة، كما يفعل التحقّق في Pocket Budget. وفي الشبكات لاحقًا في الدورة ستبقى مع throws العادية، لأن تلك الاستدعاءات قد تفشل بطرق أكثر مما تتحكّم فيه.

بهذا يكتمل قسم التصميم. صار بإمكانك الآن وصف بيانات Pocket Budget وحالات فشلها بدقة. بعد ذلك تضع هذه البيانات على الشاشة باستخدام SwiftUI.

أهم النقاط

  • صِف الطرق التي قد تفشل بها عملية ما كـ enum يتوافق مع Error، مع قيم مرتبطة للتفاصيل.
  • كل استدعاء لدالة ترمي أخطاء يُعلَّم بـ try، فيرى القارئ بالضبط أين قد يقفز التنفيذ إلى الخارج.
  • الكتلة do-catch تعالج الأخطاء؛ ويمكن لجمل catch أن تطابق حالات محدّدة، وتستقبل catch الأخيرة الباقي باسم error.
  • try? تحوّل الفشل إلى nil وتتخلّص من السبب، فاستخدمها فقط حين لا يهمّك السبب فعلًا.
  • الـ typed throws (throws(BudgetError)) تناسب الكود الذي يعالج فيه المستدعون كل حالة؛ أما throws غير المحدّدة النوع فتبقى الخيار الافتراضي الأفضل للـ APIs العامة.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

لديك func record(...) throws. ماذا يعطيك let result = try? budget.record(title: "Tea", amount: 3) حين يفشل؟

السؤال 2 من 3

دالة تستدعي try budget.record(...) لكن ليس فيها do-catch. متى تُترجَم؟

السؤال 3 من 3

متى تكون الـ typed throws، مثل throws(BudgetError)، الخيار الأفضل؟

تدريب

اختر بحكمة

تتبّع الأخطاء

يستخدم هذا المقطع CategoryBudget وBudgetError من الدرس: ترمي record الخطأ .emptyTitle للعنوان الفارغ، و.nonPositiveAmount للمبلغ الذي يساوي صفرًا أو أقل، و.overLimit(by:) حين يتجاوز المجموع الجديد الحدّ. توقّع كل ما تطبعه الحلقة.

Swift
var food = CategoryBudget(limit: 50)
let attempts: [(String, Decimal)] = [("Lunch", 12), ("", 5), ("Dinner", 45)]

for (title, amount) in attempts {
    do {
        try food.record(title: title, amount: amount)
        print("Saved \(title)")
    } catch BudgetError.overLimit(let by) {
        print("Over by \(by)")
    } catch {
        print("Rejected: \(error)")
    }
}
print(food.spent)
تتبّع الأخطاء