المعضلة الأبدية: هل نضع [unowned self] أم لا؟
\n\nإذا كنت قد كتبت يومًا بلغة Swift وواجهت الإغلاقات (closures)، فبالتأكيد تساءلت: "هل أحتاج إلى كتابة [unowned self] داخل كل إغلاق أستخدم فيه self؟".
هذا أحد أكثر الأسئلة شيوعًا على Stack Overflow (عشرات الآلاف من المشاهدات)، وليس من دون سبب. في عام 2025، مع ظهور ميزات Swift الجديدة (مثل محلل دورة الحياة المحسّن وفحوصات المترجم الأكثر صرامة)، أصبحت الإجابة أكثر دقة.
\n\nفي هذه المقالة، لن نقدم إجابة جافة بـ"نعم" أو "لا". بل سنحلل كيف تعمل إدارة الذاكرة في Swift فعليًا، ومتى يكون [unowned self] هو المنقذ، ومتى يكون قنبلة موقوتة، وما هي أفضل الممارسات التي يجب استخدامها في المشاريع الحديثة.
هيا بنا! 🚀
\n\nنبذة مختصرة عن ARC ودورات الاحتفاظ (للمبتدئين)
\n\nقبل الحديث عن [unowned self]، دعنا نتذكر كيف يعمل العد التلقائي للمراجع (Automatic Reference Counting - ARC).
ARC هي آلية لحساب المراجع. كل كائن في Swift (مثيل من كلاس) لديه عداد مراجع. عندما تنشئ مرجعًا جديدًا للكائن (مثل تعيينه لخاصية أو وضعه في مصفوفة)، يزداد العداد. عندما يختفي المرجع، يقل العداد. وعندما يصل العداد إلى الصفر، يتم تدمير الكائن.
\n\nتنشأ المشكلة عندما يشير كائنان إلى بعضهما البعض (مرجع دائري). تخيل:
\n\nclass Parent {\n var child: Child?\n}\n\nclass Child {\n var parent: Parent? // مرجع قوي يعود إلى Parent\n}\n\nإذا قمت بإنشاء زوج من Parent و Child وقمت بتعيين كلا المرجعين، فسيظلان ممسكين ببعضهما البعض إلى الأبد. لن يقوم ARC أبدًا بتصفير عدادهما. وهذا ما يسمى دورة احتفاظ (retain cycle) (دورة من المراجع القوية).
الإغلاقات هي أيضًا أنواع مرجعية. عندما يلتقط الإغلاق self (مثلًا لاستدعاء دالة)، فإنه يخزن مرجعًا قويًا للكائن. إذا كان الكائن، بدوره، يخزن مرجعًا قويًا للإغلاق (مثلًا كخاصية)، ينشأ نفس الدورة.
Weak vs Unowned: ما الفرق؟
\n\nلكسر الدورة، نستخدم weak أو unowned.
weak self
\n- \n
- دائمًا اختياري (optional) (
self?). \n - المرجع لا يزيد عداد ARC. \n
- عندما يتم تدمير الكائن، يصبح المرجع
weakتلقائيًاnil. \n - آمن — تتحقق دائمًا باستخدام
guard let self = self else { return }. \n
unowned self
\n- \n
- غير اختياري (non-optional) (
selfبدون علامة استفهام). \n - المرجع لا يزيد عداد ARC. \n
- عندما يتم تدمير الكائن، المرجع
unownedلا يصبح nil. بل يصبح مرجعًا معلقًا (dangling pointer). \n - الوصول إلى هذا المرجع بعد تدمير الكائن يسبب تعطلًا (crash) (EXC_BAD_ACCESS). \n
النقطة الأساسية: unowned هو وعد للمترجم: "أضمن أن الكائن سيكون حيًا أثناء تنفيذ هذا الإغلاق". إذا أخطأت، سيتعطل التطبيق.
متى تستخدم [unowned self] (ومتى لا)
\n\nدعنا نحلل سيناريوهات حقيقية من ممارسة تطوير iOS في عام 2025.
\n\nالحالة 1: [unowned self] مقبول (وحتى مفضل)
\n\nاستخدم [unowned self] عندما تكون متأكدًا تمامًا أن self سيكون حيًا عند تنفيذ الإغلاق. مثال نموذجي هو إغلاق يتم تنفيذه بشكل متزامن داخل دالة الكائن، والكائن مضمون ألا يتم تدميره قبل اكتمال الإغلاق.
class DataFetcher {\n var data: Data?\n \n func fetchData() {\n // هذا الإغلاق يتم تنفيذه بشكل متزامن داخل الدالة\n let processCl