الوحدة 2 · تصميم البيانات بالأنواع
الـ enums مع القيم المرتبطة
16 دقراءة 4 داملأ الفراغات
الدرس في لمحة
استبدل النصوص الهشّة بالـ enums، وأرفق بيانات بكل حالة عبر القيم المرتبطة، ودع جمل switch الشاملة تجد كل مكان يحتاج إلى معالجة حالة جديدة.
- 1الـ enums ذات القيم الخام
- 2جمل switch الشاملة ميزة
- 3القيم المرتبطة: حالات تحمل بيانات
- 4استخراج البيانات من جديد
- 5enums بدل فوضى القيم المنطقية
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
في القسم السابق كان تصنيف المصروف String. وهذا يعمل إلى أن يكتب أحدهم "Fod"، أو "food" في مكان و"Food" في مكان آخر، فيُسقط المجموع الشهري نصف مشترياتك من البقالة بصمت. لم يكن في النوع ما يحدّد النصوص الصالحة. أما الـ enum (التعداد) فيقول ذلك بالضبط: هذه هي القائمة الكاملة للقيم الممكنة، ولا شيء غيرها مسموح.
الـ enums ذات القيم الخام
enum Category: String, CaseIterable {
case food
case transport
case housing
case fun
var title: String {
rawValue.capitalized
}
var symbolName: String {
switch self {
case .food: "fork.knife"
case .transport: "bus"
case .housing: "house"
case .fun: "ticket"
}
}
}صار لـ Category أربع قيم بالضبط. اكتب .fod وستفشل الترجمة. والجزء : String يعطي كل حالة قيمة خام (raw value)، وهي افتراضيًا اسم الحالة كنص (Category.food.rawValue تساوي "food")، وهي ما ستحفظه على القرص وتقرؤه من JSON لاحقًا. أما الاتجاه المعاكس فقابل للفشل، لأن ليس كل نص تصنيفًا: Category(rawValue: "fun") تعطي .fun، وCategory(rawValue: "Fun") تعطي nil.
يطلب CaseIterable من الـ compiler أن يولّد Category.allCases، وهي مصفوفة بكل الحالات بترتيب تعريفها. سيمرّ منتقي التصنيفات في Pocket Budget عليها، فإضافة حالة تضيف صفًا إلى المنتقي دون أي جهد.
يمكن أن تحتوي الـ enums خصائص محسوبة وmethods، تمامًا مثل الـ structs. تربط symbolName كل حالة باسم أيقونة من SF Symbols، والـ switch بداخلها بلا default.
جمل switch الشاملة ميزة
لنفترض أنك أضفت case gifts الشهر القادم. ستفشل الترجمة فورًا عند symbolName مع “switch must be exhaustive”، وعند كل switch أخرى على Category في المشروع. لقد أعدّ الـ compiler قائمة مهامك. قارن ذلك بنسخة النصوص، حيث يقع التصنيف الجديد بهدوء في أي فرع default كان موجودًا، ويعرض الأيقونة الخطأ في الإنتاج.
القيم المرتبطة: حالات تحمل بيانات
القيم الخام ثابتة لكل حالة. لكن بعض البيانات تختلف من instance إلى آخر. يسجّل Pocket Budget طريقة دفع المصروف، وكل طريقة تحتاج تفاصيل مختلفة:
enum PaymentMethod {
case cash
case card(lastFour: String)
case transfer(bank: String, reference: String?)
}
let lunch: PaymentMethod = .cash
let groceries: PaymentMethod = .card(lastFour: "4242")
let rent: PaymentMethod = .transfer(bank: "Monzo", reference: "RENT-10")لكل حالة شكلها الخاص. الدفع النقدي لا يحمل شيئًا، والدفع بالبطاقة يحمل أربعة أرقام، والتحويل يحمل اسم البنك ومرجعًا اختياريًا. لو استخدمت struct لاحتجت إلى ثلاث خصائص optional وتعليق يشرح أيّها يُملأ ومتى. أما الـ enum فيجعل التركيبات غير الصالحة، مثل دفع نقدي معه اسم بنك، مستحيلة البناء أصلًا.
استخراج البيانات من جديد
تقرأ القيم المرتبطة عبر مطابقة الأنماط (pattern matching) داخل switch:
func label(for method: PaymentMethod) -> String {
switch method {
case .cash:
return "Cash"
case .card(let lastFour):
return "Card ending \(lastFour)"
case .transfer(let bank, let reference?):
return "\(bank) transfer, ref \(reference)"
case .transfer(let bank, nil):
return "\(bank) transfer"
}
}
label(for: groceries) // "Card ending 4242"تربط let lastFour القيمة المرتبطة بثابت جديد داخل تلك الحالة. وحالتا .transfer تُظهران إلى أي مدى تصل الأنماط: let reference? لا تطابق إلا حين يكون للمرجع الاختياري قيمة، وتفكّه، وnil تطابق حين لا تكون له قيمة. وما زال الـ compiler يتحقّق من أن الحالتين معًا تغطّيان كل الاحتمالات.
حين تهمّك حالة واحدة فقط، تكون if case أقصر من switch كاملة:
if case .card(let lastFour) = groceries {
print("Remind me to check card \(lastFour)")
}enums بدل فوضى القيم المنطقية
الـ enums هي الأداة الصحيحة كلما كانت القيمة «واحدة بالضبط من هذه». المثال الكلاسيكي هو تحميل البيانات. المسوّدة الأولى تبدو عادةً كثلاث خصائص منفصلة: isLoading وrates وerrorMessage. لا شيء يمنع أن تُضبط الثلاث معًا، فتضطر كل شاشة إلى تخمين أيّها يفوز. enum واحد يُلغي التخمين:
enum LoadState {
case idle
case loading
case loaded([String: Decimal])
case failed(String)
}صارت الأسعار موجودة فقط في الحالة .loaded، والرسالة فقط في .failed، وتعالج switch واحدة في الـ view كل حالة مرة واحدة. ستبني هذا بالضبط حين يبدأ Pocket Budget بجلب أسعار الصرف. استخدم enum كلما وجدت نفسك تقارن نصوصًا أو تتلاعب بقيم منطقية لا يجب أن تكون صحيحة معًا أبدًا.
صارت أنواعك الآن قادرة على وصف بيانات Pocket Budget بدقة. بعد ذلك ستمنحها قدرات مشتركة عبر الـ protocols، مثل «قابل للمقارنة» أو «له معرّف ثابت»، وتضيف سلوكًا إلى أنواع موجودة عبر الـ extensions.
أهم النقاط
- الـ enum يسرد كل القيم الصالحة لنوع ما، فيتحوّل خطأ إملائي مثل “Fod” إلى خطأ ترجمة بدل أن يكون خطأً صامتًا.
- القيم الخام (
enum Category: String) تعطي كل حالة نصًا أو رقمًا مخزّنًا للحفظ وفكّ الترميز. - القيم المرتبطة تُرفق بيانات مختلفة بحالات مختلفة، مثل آخر أربعة أرقام في
.card. - اكتب جمل switch على الـ enums بلا
default، فإضافة حالة جديدة تجعل الـ compiler يسرد كل switch يجب أن تحدّثها.