الوحدة 2 · تصميم البيانات بالأنواع
معالجة الأخطاء والـ typed throws
18 دقراءة 4 داختر بحكمة
الدرس في لمحة
صِف حالات الفشل كـ enums للأخطاء، وارمِها ومرّرها باستخدام try، وعالجها بـ do-catch، وقرّر متى تستحق الـ typed throws في Swift 6 أن تستخدمها.
- 1الأخطاء enums
- 2throw وtry
- 3معالجة الأخطاء بـ do-catch
- 4try? وtry!
- 5رسائل يفهمها المستخدم
- 6الـ typed throws في Swift 6
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
ماذا يجب أن يحدث حين يحاول المستخدم حفظ مصروف بلا عنوان، أو بمبلغ سالب، أو بمبلغ يتجاوز ميزانية الطعام لهذا الشهر دفعة واحدة؟ إعادة optional، كما فعلت في درس الـ optionals، تخبر المستدعي أن العملية فشلت لكنها لا تخبره لماذا، وPocket Budget يحتاج إلى السبب ليعرض الرسالة المناسبة. معالجة الأخطاء في Swift تجعل الفشل جزءًا من توقيع الدالة، مع أسباب مكتوبة كأنواع.
الأخطاء enums
ابدأ بسرد الطرق التي قد يفشل بها تسجيل مصروف:
import Foundation
enum BudgetError: Error, Equatable {
case emptyTitle
case nonPositiveAmount
case overLimit(by: Decimal)
}أي نوع يتوافق مع Error يمكن رميه، والـ enum ذو القيم المرتبطة هو الخيار الطبيعي: كل حالة فشلٌ واحد، وoverLimit تحمل مقدار التجاوز الذي سيقع فيه المستخدم. ليس لـ Error أي متطلّبات؛ هو فقط يعلّم النوع على أنه قابل للرمي. وسيساعد Equatable في الاختبارات في الدرس الأخير.
throw وtry
الدالة التي قد تفشل تصرّح بذلك عبر throws وتتوقّف عبر throw:
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
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، وسيصعد الخطأ إلى من استدعاها.
try? وtry!
هناك اختصاران. try? تحوّل النتيجة إلى optional: النجاح يعطي قيمة، والفشل يعطي nil. أما try! فتؤكّد أن الاستدعاء لا يمكن أن يفشل، وتُسقط التطبيق إن فشل، بالمخاطرة نفسها التي في الفكّ القسري.
رسائل يفهمها المستخدم
print(error) تعرض overLimit(by: 12)، وهذا مقبول في السجلّ لكنه بلا فائدة في تنبيه. اجعل النوع يتوافق مع LocalizedError لتقدّم نصًا يفهمه البشر:
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 (الرمي محدّد النوع)، حيث يسمّي التوقيع النوع الدقيق:
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) لتصرّح بذلك صراحةً:
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 العامة.