الوحدة 2 · تصميم البيانات بالأنواع
الـ protocols والـ extensions
16 دقراءة 4 داكتشف الخطأ
الدرس في لمحة
عرّف الـ protocols كعقود، وتبنَّ الـ protocols القياسية مثل Identifiable وHashable، وأضف سلوكًا افتراضيًا عبر الـ protocol extensions، ووسّع أنواعًا لا تملكها.
- 1كتابة protocol
- 2protocols قياسية ستستخدمها كل يوم
- 3سلوك افتراضي عبر الـ protocol extensions
- 4some وany
- 5توسيع أنواع لا تملكها
- 6أين ستقابل الـ protocols بعد ذلك
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
يوشك Pocket Budget أن يضيف نوعًا ثانيًا من التكاليف. إلى جانب المصاريف التي تحدث مرة واحدة هناك الاشتراكات، التي تُدفع شهريًا أو سنويًا. لا يجب أن تهتم شاشة الملخّص بأيّهما أمامها: تحتاج من كلٍّ منهما عنوانًا وتكلفة شهرية. الوراثة ستُجبر الاثنين على الدخول في هرمية classes. أما جواب Swift فهو الـ protocol (البروتوكول): قائمة متطلّبات يستطيع أي نوع أن يتعهّد بتحقيقها، سواء كان struct أو class أو enum.
كتابة protocol
import Foundation
protocol BudgetItem {
var title: String { get }
var monthlyCost: Decimal { get }
}{ get } تعني «قابلة للقراءة». يستطيع النوع المتوافق أن يحقّقها بخاصية مخزّنة أو محسوبة. إليك نوعين متوافقين، يعتمدان على الـ enum المسمّى Category من الدرس السابق:
struct Expense: Identifiable, Hashable, BudgetItem {
let id = UUID()
var title: String
var amount: Decimal
var category: Category
var date: Date
var monthlyCost: Decimal { amount }
}
struct Subscription: BudgetItem {
var title: String
var price: Decimal
var billedYearly: Bool
var monthlyCost: Decimal {
billedYearly ? price / 12 : price
}
}احذف monthlyCost وسيوقفك الـ compiler بالرسالة “type ‘Subscription’ does not conform to protocol ‘BudgetItem’”، ويعرض عليك Xcode إضافة هياكل فارغة لكل ما ينقص.
protocols قياسية ستستخدمها كل يوم
يتبنّى Expense أيضًا اثنين من الـ protocols في المكتبة القياسية. Identifiable يتطلّب خاصية id، ويستخدمها List وForEach في SwiftUI للتمييز بين الصفوف، لذلك ستراه على معظم النماذج في هذه الدورة. وHashable يسمح بوضع المصاريف في Set أو استخدامها كمفاتيح في قاموس، وهو يتضمّن Equatable الذي يعطيك ==.
لم تكتب == ولا دالة hash. في الـ structs والـ enums، تولّد Swift التوافق مع Equatable وHashable تلقائيًا حين تكون كل خاصية مخزّنة متوافقة أصلًا، وUUID وString وDecimal وDate وCategory كلها كذلك. ولأن id جزء من هذه المقارنة، يبقى مصروفان بالعنوان والمبلغ نفسيهما مصروفين مختلفين، وهذا ما يعنيه المستخدم.
سلوك افتراضي عبر الـ protocol extensions
كل عنصر في الميزانية يحتاج سعرًا منسّقًا. بدل أن تكتبه مرتين، وسّع الـ protocol:
extension BudgetItem {
func formattedCost(currencyCode: String = "USD") -> String {
monthlyCost.formatted(.currency(code: currencyCode))
}
}
let coffee = Expense(title: "Coffee", amount: 4.5, category: .food, date: .now)
let music = Subscription(title: "Music", price: 120, billedYearly: true)
coffee.formattedCost() // "$4.50"
music.formattedCost(currencyCode: "EUR") // "€10.00"كل نوع يتوافق مع BudgetItem، الآن أو في المستقبل، يحصل على formattedCost دون أي جهد. هذا هو جوهر ما يقصده الناس بـ Swift الموجّهة بالـ protocols: protocols صغيرة، وسلوك مشترك في الـ extensions، تتبنّاها أنواع القيم.
some وany
كلمتان مفتاحيتان تصفان «قيمة من نوع ما متوافق»:
let items: [any BudgetItem] = [coffee, music]
let monthly = items.reduce(0) { $0 + $1.monthlyCost } // 14.5
func cheapest(_ list: [some BudgetItem]) -> String? {
list.min { $0.monthlyCost < $1.monthlyCost }?.title
}any BudgetItem صندوق يمكن أن يحمل نوعًا متوافقًا مختلفًا في كل خانة، وهذا ما تحتاجه القائمة المختلطة. أما some BudgetItem فتعني «نوعًا واحدًا محدّدًا متوافقًا، يقرّره المستدعي»: تعمل cheapest مع [Expense] أو مع [Subscription]، لكن ليس مع خليط منهما. فضّل some حين تناسب، لأن الـ compiler يعرف النوع بالضبط ويستطيع تحسين الأداء. وتستخدم SwiftUI الكلمة نفسها في var body: some View لكن في موضع القيمة المُعادة، حيث تنقلب الأدوار: تنفيذ الـ view هو الذي يختار النوع المحدّد الوحيد، ولا يعرف المستدعي عنه إلا أنه View.
توسيع أنواع لا تملكها
الـ extensions (الامتدادات) لا تقتصر على الـ protocols. يمكنك إضافة خصائص محسوبة وmethods وتوافقات إلى أي نوع، بما في ذلك أنواع Apple:
extension Decimal {
var asCurrency: String { formatted(.currency(code: "USD")) }
}
extension Expense: CustomStringConvertible {
var description: String { "\(title) (\(category.rawValue)): \(formattedCost())" }
}
print(coffee) // Coffee (food): $4.50كثير من فرق Swift تستخدم الـ extensions أيضًا لتنظيم الملف: الخصائص المخزّنة للنوع في الأعلى، ثم extension واحد لكل توافق. وهناك قيد واحد ينطبق في كل مكان: الـ extension لا يستطيع إضافة خصائص مخزّنة، بل محسوبة فقط. كذلك حين تغيّر method في extension على struct إحدى الخصائص، فما زالت تحتاج إلى mutating، تمامًا كأي method في التعريف الأصلي.
أين ستقابل الـ protocols بعد ذلك
الـ protocols هي طريقة أطر عمل Apple في التحدّث مع كودك. في SwiftUI، كل شاشة تبنيها هي struct يتوافق مع الـ protocol المسمّى View، ومتطلّبه الوحيد خاصية body. ويطلب List عناصر Identifiable حتى يتتبّع الصفوف. ويطلب كود الشبكات Codable حتى يتحوّل JSON إلى أنواعك. وتطلب الـ concurrency في Swift 6 الـ protocol المسمّى Sendable قبل أن تعبر القيمة بين الـ threads. وفي كل مرة يتكرّر النمط نفسه من هذا الدرس: يحدّد إطار العمل عقدًا، ويتعهّد نوعك بتحقيقه، ويتحقّق الـ compiler من الوعد.
صارت لديك الآن عدّة التصميم الكاملة: الـ structs والـ classes والـ enums والـ protocols. القطعة الأخيرة قبل SwiftUI هي ما يحدث حين تفشل عملية ما، مثل مصروف سيتجاوز الميزانية، وكيف تجعل معالجة الأخطاء في Swift الفشلَ جزءًا من نوع الدالة.
أهم النقاط
- الـ protocol يسرد متطلّبات؛ وأي struct أو class أو enum يحقّقها يمكن استخدامه في كل مكان يُتوقَّع فيه ذلك الـ protocol.
- في الـ structs والـ enums، تولّد Swift التوافق مع
EquatableوHashableتلقائيًا حين تكون كل الخصائص المخزّنة متوافقة معهما أصلًا. - الـ protocol extension يعطي كل نوع متوافق تنفيذًا افتراضيًا دون أي جهد.
- الـ extensions تضيف methods وخصائص محسوبة وتوافقات إلى أي نوع، بما في ذلك أنواع Apple، لكنها لا تضيف أبدًا خصائص مخزّنة جديدة.
some Protocolتمثّل نوعًا واحدًا محدّدًا متوافقًا (في المعامل يختاره المستدعي)؛ أماany Protocolفصندوق يمكن أن يحمل نوعًا متوافقًا مختلفًا في كل مرة.