الوحدة 2 · تصميم البيانات بالأنواع
الـ structs والـ classes وسلوك القيمة
17 دقراءة 4 داختر بحكمة
الدرس في لمحة
توقّع سلوك الـ structs والـ classes عند نسخها، واستخدم الـ mutating methods بشكل صحيح، واكسر دورة الاحتفاظ التي يسبّبها closure باستخدام weak self.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
تفتح مصروفًا، وتغيّر مبلغه من 4.50 إلى 5.00 في شاشة التعديل، ثم تنقر Cancel. هل يجب أن تبقى القائمة تعرض 4.50؟ مع الـ struct (البنية) يحدث ذلك تلقائيًا. أما مع الـ class (الصنف)، فالقائمة تعرض 5.00 بالفعل، لأن شاشة التعديل والقائمة كانتا تنظران إلى الكائن نفسه. هذا الفرق الواحد، سلوك القيمة مقابل سلوك المرجع، يحدّد سلوك تطبيق iOS أكثر من أي قرار تصميمي آخر.
الـ structs تُنسَخ
هذا هو نوع المصروف، وخصائصه الآن معرّفة بـ var حتى يمكن تعديله:
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) // 5var draft = original تنتج نسخة كاملة مستقلّة. تعديل المسوّدة لا يمكن أن يؤثّر في الأصل، فتستطيع شاشة التعديل أن تعمل على مسوّدة، ولا يكلّف زر Cancel شيئًا: ترمي المسوّدة وحسب. تعطي Swift كل struct مُهيِّئًا مجانيًا اسمه memberwise initializer، ولهذا يعمل Expense(title:amount:) دون أن تكتب init.
النصوص والمصفوفات والقواميس وDecimal وInt كلها structs. لهذا أعطتك var copy = amounts في درس المجموعات مصفوفة مستقلّة. وتجعل Swift هذه النسخ رخيصة بمشاركة التخزين إلى أن يغيّره أحد الطرفين فعلًا.
الـ classes تُشارَك
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 الحديثة.
let تعني أشياء مختلفة
let تجمّد ما يحمله الاسم. في الـ struct يحمل الاسم القيمة كلها، فلا يمكن تغيير أي شيء بداخلها. وفي الـ class يحمل الاسم مرجعًا، فيكون المرجع ثابتًا لكن خصائص الكائن المعرّفة بـ var تبقى قابلة للتغيير، وهذا بالضبط سبب ترجمة sameAccount.balance -= 50 رغم أن sameAccount معرّف بـ let.
الـ methods (الدوال التابعة) في الـ struct التي تغيّر الخصائص يجب أن تصرّح بذلك عبر mutating:
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 يلتقط الكائن نفسه:
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]لكسرها.