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

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

الـ protocols والـ extensions

16 دقراءة 4 داكتشف الخطأ

الدرس في لمحة

عرّف الـ protocols كعقود، وتبنَّ الـ protocols القياسية مثل Identifiable وHashable، وأضف سلوكًا افتراضيًا عبر الـ protocol extensions، ووسّع أنواعًا لا تملكها.

  1. 1كتابة protocol
  2. 2protocols قياسية ستستخدمها كل يوم
  3. 3سلوك افتراضي عبر الـ protocol extensions
  4. 4some وany
  5. 5توسيع أنواع لا تملكها
  6. 6أين ستقابل الـ protocols بعد ذلك

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

يوشك Pocket Budget أن يضيف نوعًا ثانيًا من التكاليف. إلى جانب المصاريف التي تحدث مرة واحدة هناك الاشتراكات، التي تُدفع شهريًا أو سنويًا. لا يجب أن تهتم شاشة الملخّص بأيّهما أمامها: تحتاج من كلٍّ منهما عنوانًا وتكلفة شهرية. الوراثة ستُجبر الاثنين على الدخول في هرمية classes. أما جواب Swift فهو الـ protocol (البروتوكول): قائمة متطلّبات يستطيع أي نوع أن يتعهّد بتحقيقها، سواء كان struct أو class أو enum.

كتابة protocol

BudgetItem.swift
import Foundation

protocol BudgetItem {
    var title: String { get }
    var monthlyCost: Decimal { get }
}

{ get } تعني «قابلة للقراءة». يستطيع النوع المتوافق أن يحقّقها بخاصية مخزّنة أو محسوبة. إليك نوعين متوافقين، يعتمدان على الـ enum المسمّى Category من الدرس السابق:

Swift
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:

Swift
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، تتبنّاها أنواع القيم.

يتوافق Expense وSubscription كلاهما مع الـ protocol المسمّى BudgetItem، الذي يتطلّب title وmonthlyCost. ويضيف protocol extension الدالة formattedCost إلى كل نوع متوافق. protocol BudgetItem title, monthlyCost formattedCost() من الـ extension struct Expense struct Subscription يتوافق مع
نوعان، protocol واحد، وسلوك مشترك

some وany

كلمتان مفتاحيتان تصفان «قيمة من نوع ما متوافق»:

Swift
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:

Swift
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 فصندوق يمكن أن يحمل نوعًا متوافقًا مختلفًا في كل مرة.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

للـ struct المعرّف struct Expense: Hashable خصائص من الأنواع UUID وString وDecimal. ماذا عليك أن تكتب ليصبح hashable؟

السؤال 2 من 4

لماذا لا تستطيع إضافة var note: String = "" إلى Decimal داخل extension؟

السؤال 3 من 4

تحتاج مصفوفة واحدة تحمل قيمًا من Expense وSubscription معًا، وكلاهما يتوافق مع BudgetItem. أيّ نوع تعلنه؟

السؤال 4 من 4

يعرّف protocol extension الدالة formattedCost(). يتوافق Subscription مع الـ protocol لكنه لا ينفّذها. ماذا يحدث حين تستدعي music.formattedCost()؟

تدريب

اكتشف الخطأ

الـ extension الذي لا يُترجَم

نقل أحد زملائك سلوك Budget إلى extension، والآن لا يُبنى المشروع. اختر السطر الذي يرفضه الـ compiler.

حدّد السطر أو الأسطر التي تظنها خاطئة، ثم تحقّق.