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

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

الـ structs والـ classes وسلوك القيمة

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

الدرس في لمحة

توقّع سلوك الـ structs والـ classes عند نسخها، واستخدم الـ mutating methods بشكل صحيح، واكسر دورة الاحتفاظ التي يسبّبها closure باستخدام weak self.

  1. 1الـ structs تُنسَخ
  2. 2الـ classes تُشارَك
  3. 3let تعني أشياء مختلفة
  4. 4أيّهما تختار
  5. 5ARC ودورات الاحتفاظ

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

تفتح مصروفًا، وتغيّر مبلغه من 4.50 إلى 5.00 في شاشة التعديل، ثم تنقر Cancel. هل يجب أن تبقى القائمة تعرض 4.50؟ مع الـ struct (البنية) يحدث ذلك تلقائيًا. أما مع الـ class (الصنف)، فالقائمة تعرض 5.00 بالفعل، لأن شاشة التعديل والقائمة كانتا تنظران إلى الكائن نفسه. هذا الفرق الواحد، سلوك القيمة مقابل سلوك المرجع، يحدّد سلوك تطبيق iOS أكثر من أي قرار تصميمي آخر.

الـ structs تُنسَخ

هذا هو نوع المصروف، وخصائصه الآن معرّفة بـ var حتى يمكن تعديله:

Expense.swift
import Foundation

struct Expense {
    var title: String
    var amount: Decimal
}

var original = Expense(title: "Coffee", amount: 4.5)
var draft = original
draft.amount = 5

print(original.amount)   // 4.5
print(draft.amount)      // 5

var draft = original تنتج نسخة كاملة مستقلّة. تعديل المسوّدة لا يمكن أن يؤثّر في الأصل، فتستطيع شاشة التعديل أن تعمل على مسوّدة، ولا يكلّف زر Cancel شيئًا: ترمي المسوّدة وحسب. تعطي Swift كل struct مُهيِّئًا مجانيًا اسمه memberwise initializer، ولهذا يعمل Expense(title:amount:) دون أن تكتب init.

النصوص والمصفوفات والقواميس وDecimal وInt كلها structs. لهذا أعطتك var copy = amounts في درس المجموعات مصفوفة مستقلّة. وتجعل Swift هذه النسخ رخيصة بمشاركة التخزين إلى أن يغيّره أحد الطرفين فعلًا.

الـ classes تُشارَك

Swift
final class Account {
    var balance: Decimal

    init(balance: Decimal) {
        self.balance = balance
    }
}

let checking = Account(balance: 500)
let sameAccount = checking
sameAccount.balance -= 50

print(checking.balance)          // 450
print(checking === sameAccount)  // true: same instance

الـ instance (النسخة) من class يعيش في مكان واحد؛ والمتغيّرات تحمل مراجع إليه. let sameAccount = checking تنسخ المرجع، فيشير الاسمان إلى الحساب نفسه. والعامل === يسأل «هل هذا هو الـ instance نفسه تمامًا؟»، وهو سؤال لا معنى له مع الـ structs. تحتاج الـ classes إلى init صريح للخصائص المخزّنة التي لا قيم افتراضية لها. علّمها بـ final إلا إذا كنت تخطّط للوراثة منها، وهذا نادر في Swift الحديثة.

على اليسار، original وdraft صندوقا Expense منفصلان بمبلغين 4.5 و5. على اليمين، checking وsameAccount اسمان يشير سهماهما إلى صندوق Account واحد مشترك رصيده 450. struct: قيمتان original المبلغ 4.5 draft المبلغ 5 نسختان مستقلّتان class: كائن واحد checking sameAccount Account الرصيد 450
نسخ struct مقابل نسخ مرجع إلى class

let تعني أشياء مختلفة

let تجمّد ما يحمله الاسم. في الـ struct يحمل الاسم القيمة كلها، فلا يمكن تغيير أي شيء بداخلها. وفي الـ class يحمل الاسم مرجعًا، فيكون المرجع ثابتًا لكن خصائص الكائن المعرّفة بـ var تبقى قابلة للتغيير، وهذا بالضبط سبب ترجمة sameAccount.balance -= 50 رغم أن sameAccount معرّف بـ let.

الـ methods (الدوال التابعة) في الـ struct التي تغيّر الخصائص يجب أن تصرّح بذلك عبر mutating:

Swift
struct Budget {
    var limit: Decimal
    var spent: Decimal = 0

    var remaining: Decimal { limit - spent }   // computed property

    mutating func record(_ amount: Decimal) {
        spent += amount
    }
}

var food = Budget(limit: 300)
food.record(42)
print(food.remaining)   // 258

أيّهما تختار

توصية Apple، وتوصيتي أنا، هي أن تبدأ بـ struct. القيم أسهل في التفكير لأن لا شيء يستطيع تغييرها من وراء ظهرك، وهي آمنة للتمرير بين الـ threads، وهذا مهم جدًا في Swift 6. استخدم class حين تحتاج إلى هوية: شيء واحد مشترك تراه أجزاء متعدّدة من التطبيق وتحدّثه. في Pocket Budget، كل Expense هو struct، أما المخزن الذي يملك القائمة وتراقبه عدة شاشات فسيكون class. والماكرو @Observable من إطار Observation و@Model من SwiftData، اللذان ستستخدمهما لاحقًا، يتطلّبان classes لهذا السبب تحديدًا.

اختبار سريع حين لا تكون متأكّدًا: تخيّل أنك تسلّم نسخة إلى شاشة أخرى. إن كان يجب أن تحصل تلك الشاشة على نسختها الخاصة لتعدّلها بحرية، فأنت تريد struct. وإن كان يجب أن ترى كل تغيير يجريه باقي التطبيق، وأن تظهر تغييراتها في كل مكان، فأنت تريد class. معظم البيانات في معظم التطبيقات تنجح في الاختبار الأول.

ARC ودورات الاحتفاظ

تُحرَّر الـ classes عبر العدّ التلقائي للمراجع (ARC): كل مرجع قوي يضيف واحدًا إلى عدّاد، وحين يصل العدّاد إلى الصفر يُحرَّر الكائن ويُنفَّذ deinit الخاص به. تبدأ المشكلة حين يخزّن كائنٌ closure يلتقط الكائن نفسه:

BudgetAlert.swift
final class BudgetAlert {
    let threshold: Decimal
    var onTrigger: (() -> Void)?

    init(threshold: Decimal) {
        self.threshold = threshold
    }

    func arm() {
        onTrigger = { [weak self] in
            guard let self else { return }
            print("Spent more than \(self.threshold)")
        }
    }

    deinit { print("BudgetAlert freed") }
}

بدون [weak self]، يمتلك التنبيهُ الـ closure ويمتلك الـ closure التنبيه. لا يصل أيّ من العدّادين إلى الصفر أبدًا، ولا يُنفَّذ deinit، وتكون قد سرّبت ذاكرة. قائمة الالتقاط [weak self] تجعل مرجع الـ closure ضعيفًا واختياريًا، فيفكّه guard let self ما دام الكائن حيًّا. الـ struct وحده لا يستطيع تكوين هذه الدورات، وهذا سبب آخر لكون الـ structs الخيار الافتراضي.

القيمة أو المرجع نصف عملية التصميم. والنصف الآخر هو وصف بيانات تأتي بأشكال مختلفة، مثل مصروف مدفوع بالبطاقة مقابل آخر مدفوع نقدًا. لهذا وُجدت الـ enums ذات القيم المرتبطة، وهي موضوع الدرس التالي.

أهم النقاط

  • إسناد struct ينتج نسخة مستقلّة؛ أما إسناد instance من class فينتج مرجعًا ثانيًا إلى الكائن نفسه.
  • الـ struct المعرّف بـ let مجمّد بالكامل، بينما المرجع إلى class المعرّف بـ let ما زال يسمح بتغيير خصائص الكائن المعرّفة بـ var.
  • الـ methods في الـ struct التي تغيّر الخصائص يجب أن تُعلَّم بـ mutating، ولا تُستدعى إلا على var.
  • اجعل الـ struct خيارك الافتراضي للبيانات؛ واستخدم class حين تحتاج إلى هوية مشتركة، مثل كائن واحد تراقبه شاشات كثيرة.
  • الـ class الذي يخزّن closure يلتقط self التقاطًا قويًا ينشئ دورة احتفاظ (retain cycle)؛ التقط [weak self] لكسرها.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

Budget هو struct فيه mutating func record(_:). لماذا لا يُترجَم let food = Budget(limit: 300); food.record(42)؟

السؤال 2 من 4

Account هو class. لديك let a = Account(balance: 500); let b = a; b.balance -= 50. ما قيمة a.balance؟

السؤال 3 من 4

أيّ البيانات في Pocket Budget هي الأنسب لأن تكون class بدل struct؟

السؤال 4 من 4

يخزّن class القيمة onTrigger = { print(self.threshold) } في إحدى خصائصه. ما النتيجة؟

تدريب

اختر بحكمة

struct أم class: توقّع الناتج

Limit هو struct وWallet هو class. اقرأ المقطع وتوقّع السطر الوحيد الذي يطبعه.

Swift
import Foundation

struct Limit {
    var amount: Decimal
}

final class Wallet {
    var balance: Decimal
    init(balance: Decimal) { self.balance = balance }
}

var food = Limit(amount: 300)
var fun = food
fun.amount = 100

let mine = Wallet(balance: 500)
let shared = mine
shared.balance -= 50

print(food.amount, fun.amount, mine.balance)
struct أم class: توقّع الناتج