يمكن لبنية Swift مشاركة كائنات قابلة للتغيير بعد الإسناد. ولإبقاء النسخ مستقلة، استخدم خصائص من أنواع القيم أو أخفِ تخزين الفئة خلف النسخ عند الكتابة. يبني هذا الدليل حاسبة تجريبية لصف من البلاطات لمعاينة على سطح مكتب Mac، ويتحقق من أن تعديل نسخة واحدة لا يغيّر النسخ الأخرى.

لماذا يظل نسخ البنية يغيّر الأصل؟

لنفترض أن معاينة على سطح المكتب تحسب عرض صف من البلاطات. تنسخ إعداداتها لتجربة عرض آخر للبلاطة، وتتوقع أن تظل المعاينة الأصلية كما هي. لكن هذا التوقع لا يتحقق عندما تحتوي قيمتا الإعدادات على مرجع إلى الكائن القابل للتغيير نفسه.

تنسخ Swift القيم المخزنة في البنية عند إسنادها إلى متغير آخر. أما في خاصية من فئة، فالقيمة المخزنة هي مرجع، ويظل الكائن المشار إليه مشتركًا. يظل الغلاف بنية، لكن سلوكه الملحوظ قد يتضمن تغييرات مشتركة. وتنبع هذه القواعد من التمييز الموثق في Swift بين أنواع القيم وأنواع المراجع.

يوضح هذا المثال الكامل المشكلة. وهو يستخدم مكتبة Swift القياسية فقط، لذا لا يتطلب أي استيرادات.

final class TileDimensions {
    var width: Int

    init(width: Int) {
        self.width = width
    }
}

struct SharedLayout {
    let dimensions: TileDimensions
}

let original = SharedLayout(
    dimensions: TileDimensions(width: 40)
)
let edited = original
edited.dimensions.width = 60

assert(original.dimensions === edited.dimensions)
assert(original.dimensions.width == 60)
print(original.dimensions.width) // 60

إن تعريف الخاصية أو الغلاف باستخدام let لا يجمّد الخصائص القابلة للتغيير في الكائن. فهو يمنع استبدال ذلك المرجع أو قيمة البنية. ويميز فصل البنى والفئات في Swift صراحةً بين المراجع الثابتة وحالة الكائن غير القابلة للتغيير.

كما أن وضع هذه الكائنات في مصفوفة يحافظ على هويتها المشتركة. تدير نسخ المصفوفات عناصرها بصورة مستقلة، لكن العنصر الذي قيمته فئة هو بحد ذاته مرجع. يؤثر استبدال عنصر في مصفوفة واحدة؛ أما تعديل كائن تشير إليه المصفوفتان فيُلاحظ عبر كلتيهما. وتوضح Apple هذا التمييز في توثيق Array.

أي تصميم يمنح الحساب قيمًا مستقلة؟

حدّد العقد قبل اختيار التنفيذ: بعد var edited = original، يجب أن يترك كل تعديل مدعوم على أي من التخطيطين العرض المحسوب للتخطيط الآخر دون تغيير. ينبغي ألا يحتاج قارئو النوع إلى معرفة ما إذا كان تخزينه مشتركًا داخليًا.

ينطبق دليل القرار التالي على هذا العقد في نموذج معاينة محلي. وهو توصية تصميم مستمدة من قواعد النسخ الموثقة.

الحالة التصميم المناسب الشرط اللازم للحفاظ على الاستقلالية
تتكون الإعدادات من أعداد أو سلاسل أو مكونات أخرى ذات دلالات قيمية خزّن هذه القيم مباشرةً في البنية تحقّق من أن المكونات المتداخلة توفر أيضًا قيمًا مستقلة يمكن ملاحظتها
يجب الإبقاء على التخزين الحالي المدعوم بفئة استخدم تخزينًا خاصًا مع النسخ عند الكتابة يجب أن يفصل كل تعديل الحالة القابلة للتغيير المشتركة أولًا
لا تلزم لقطة منفصلة إلا عند عملية محددة بالاسم وفّر عملية نسخ صريحة وثّق أن الإسناد العادي يظل يشارك الكائنات ما لم يفرض الغلاف الاستقلالية أيضًا
تعدّل أجزاء متعددة من البرنامج كائنًا واحدًا عمدًا أبقِ على دلالات المراجع صِف سلوك الكائن المشترك في واجهة برمجة التطبيقات

بالنسبة إلى ثلاثة إعدادات صحيحة، تكون خصائص البنية المباشرة الخيار الأبسط. يفيد التنفيذ المدعوم بفئة أدناه عند تكييف تصميم تخزين موجود أو تعلّم كيفية عمل الحد الفاصل؛ لكنه لا يثبت وجود ميزة أداء لهذا الحساب الصغير.

تتيح النسخ عند الكتابة لقيمتين مشاركة التخزين حتى تتغير إحداهما. تصف Apple تغليف فئة داخل بنية وتطبيق النسخ عند الكتابة بوصفه طريقة للجمع بين تخزين مخصص منفصل ودلالات القيم في استكشاف أداء Swift. مجرد تغليف الفئة لا ينفّذ هذا السلوك.

كما أن طريقة copy() الصريحة وحدها تترك ثغرة: فلا يزال بإمكان المستدعين إجراء إسناد عادي دون استدعائها. إذا كان الإسناد جزءًا من عقد القيم الخاص بك، فيجب أن تفرض مسارات التعديل في الغلاف الاستقلالية تلقائيًا.

كيف تنفّذ النسخ عند الكتابة وتتحقق منه؟

استخدم كائن تخزين خاصًا واحدًا، وأنشئه داخل الغلاف، ولا تكشف إلا القيم المحسوبة وطرق التعديل المضبوطة. استدعِ isKnownUniquelyReferenced مباشرةً قبل تغيير التخزين. إذا لم يثبت أن المرجع فريد، فاستبدل التخزين بنسخة مستقلة. توثّق Apple هذا التسلسل تحديدًا في تنفيذ النسخ عند الكتابة.

يستخدم الحساب المشروح مدخلات توضيحية: ثلاث بلاطات، عرض كل منها 40 نقطة، مع فجوة قدرها 10 نقاط بين كل بلاطتين متجاورتين. ثمة فجوتان، لذا يبلغ عرض الصف 3 * 40 + 2 * 10 = 140 نقطة. تؤدي زيادة عرض البلاطة إلى 60 إلى عرض قدره 200 نقطة.

لتجربة البرنامج الكامل على Mac، شغّل أولًا swift --version في Terminal للتأكد من توفر سلسلة أدوات Swift، كما يوصي دليل البدء في Swift. احفظ الشيفرة التالية باسم Layout.swift، ثم شغّل swift Layout.swift من المجلد الذي يحتوي عليه. يحسب البرنامج النتائج في الذاكرة ويطبعها.

struct TileLayout {
    private final class Storage {
        let columns: Int
        var tileWidth: Int
        var gap: Int

        init(columns: Int, tileWidth: Int, gap: Int) {
            self.columns = columns
            self.tileWidth = tileWidth
            self.gap = gap
        }

        func copy() -> Storage {
            Storage(
                columns: columns,
                tileWidth: tileWidth,
                gap: gap
            )
        }
    }

    private var storage: Storage

    init(columns: Int, tileWidth: Int, gap: Int) {
        precondition((1...1000).contains(columns))
        precondition((0...1000).contains(tileWidth))
        precondition((0...1000).contains(gap))
        storage = Storage(
            columns: columns,
            tileWidth: tileWidth,
            gap: gap
        )
    }

    var rowWidth: Int {
        storage.columns * storage.tileWidth
            + (storage.columns - 1) * storage.gap
    }

    private mutating func ensureUniqueStorage() {
        if !isKnownUniquelyReferenced(&storage) {
            storage = storage.copy()
        }
    }

    mutating func setTileWidth(_ width: Int) {
        precondition((0...1000).contains(width))
        ensureUniqueStorage()
        storage.tileWidth = width
    }

    mutating func setGap(_ gap: Int) {
        precondition((0...1000).contains(gap))
        ensureUniqueStorage()
        storage.gap = gap
    }
}

var original = TileLayout(columns: 3, tileWidth: 40, gap: 10)
var edited = original

edited.setTileWidth(60)
assert(original.rowWidth == 140)
assert(edited.rowWidth == 200)

original.setGap(5)
assert(original.rowWidth == 130)
assert(edited.rowWidth == 200)

let snapshot = edited
edited.setGap(20)
assert(snapshot.rowWidth == 200)
assert(edited.rowWidth == 220)

print(original.rowWidth, snapshot.rowWidth, edited.rowWidth)
// Expected output: 130 200 220

لا يتطلب البرنامج استيرادات أو حزمًا خارجية. النطاقات المقبولة قيود توضيحية مقصودة: من عمود واحد إلى 1,000 عمود، ومن صفر إلى 1,000 نقطة للعرض والفجوة. وهي تُبقي حساب الأعداد الصحيحة ضمن حدود معينة. يؤدي تمرير وسيط خارج النطاق إلى الإخفاق في شرط مسبق؛ ولا تُقلَّم قيمته بصمت. إذا أصبحت هذه المحددات تقبل لاحقًا نصًا قابلًا للتحرير من واجهة سطح مكتب، فتحقّق من ذلك الإدخال قبل استدعائها، وأتح للمستخدم طريقة لتصحيح الخطأ.

تغطي عمليات التحقق ثلاثة متطلبات يمكن ملاحظتها: تعديل التخطيط المنسوخ يحافظ على الأصل، وتعديل الأصل يحافظ على النسخة، والاحتفاظ بلقطة ثابتة يحافظ على نتيجتها أثناء تعديل لاحق. وهي تختبر السلوك دون اشتراط عدد محدد من عمليات التخصيص. لم تكن Swift متاحة في بيئة البحث، لذا جرى فحص هذا المثال دون تشغيله هناك؛ والنتيجة المعروضة هي النتيجة المتوقعة.

ما الذي يجب أن يظل صحيحًا عند توسيع الغلاف؟

يأتي الاستقلال في المثال من حدود واجهة برمجة التطبيقات بأكملها. التخزين خاص، ولا يقبل أي مُهيّئ كائن تخزين يحتفظ به طرف خارجي، ولا تعيد أي دالة قراءة كائنًا من هذا النوع. ويمر كلا أسلوبي التعديل بعملية الفصل نفسها. تحسب دالة القراءة نتيجة دون تغيير التخزين.

عند إضافة ميزات، تتبّع كل مسار جديد يمكنه تغيير الحالة الملحوظة. يحتاج أي محدد أو فهرس فرعي أو رد نداء للتحرير جديد إلى المعالجة نفسها التي تتلقاها الطرق الحالية. إن أعادت دالة قراءة كائنًا فرعيًا قابلًا للتغيير، فسيتمكن المستدعون من تجاوز الفصل، حتى لو كانت دالة القراءة نفسها للقراءة فقط.

يجب أن تغطي نسخة التخزين أيضًا كل الحالة القابلة للتغيير ذات الصلة بالقيمة. في هذا المثال، تنسخ Storage.copy() ثلاثة أعداد صحيحة، وهذا كافٍ. إذا احتوى التخزين لاحقًا على عنصر فرعي من فئة قابل للتغيير، فإن إسناد ذلك العنصر إلى التخزين الجديد يُبقي مرجعًا مشتركًا آخر. انسخ ذلك العنصر الفرعي بالشكل المناسب، أو استبدله بمكوّن يوفر دلالات القيم أصلًا. ويوضح السلوك الموثق لعناصر الفئات في Array الحد الفاصل نفسه.

اتبع هذا التسلسل عند توسيع الحاسبة:

  1. انسخ تخطيطًا قبل تجربة كل عملية تحرير جديدة.
  2. تحقّق من أن التخطيط الذي لم تُعدّله يحتفظ بنتيجته السابقة.
  3. كرر ذلك مع تبديل الأدوار.
  4. احتفظ بلقطة، ونفّذ عدة تعديلات لاحقة، ثم افحصها مجددًا.
  5. افحص كل خاصية مُعادة ووسيط مُهيّئ بحثًا عن مراجع قابلة للتغيير قد تتسرب من الغلاف.

تمنع فئة تخزين final الفئات الفرعية من إضافة سلوك، لكنها لا تجعل خصائصها غير قابلة للتغيير. وبالمثل، يتيح وضع mutating على عملية تحديث البنية؛ أما منطق الفصل فهو ما يحافظ على الاستقلال في هذا المثال.

كيف ينبغي نسخ كائنات Foundation؟

إذا كان نموذج موجود يخزّن كائنات Foundation مثل NSMutableString، فاستخدم سلوك النسخ الموثق للنوع المحدد عند تنفيذ فصل التخزين. لا تستدعي Swift تلقائيًا طرق النسخ في Foundation عند الإسناد.

يعرّف بروتوكول Foundation ‏NSCopying النسخ الوظيفي، بينما يعتمد المعنى الدقيق على الفئة. كما يسمح توثيقه بإعادة الكائن الأصلي عندما تكون الفئة ومحتوياتها غير قابلة للتغيير. لذلك، لا تُعد هوية الكائن وحدها اختبارًا عامًا على أن النسخة تفي بعقد القيم الخاص بك.

عندما تحتاج إلى نتيجة قابلة للتغيير من نوع له شكلان، أحدهما قابل للتغيير والآخر غير قابل للتغيير، فارجع إلى NSMutableCopying. توثّق Apple mutableCopy(with:) والطريقة الملائمة mutableCopy() لهذا الغرض. أما الفئات التي لا تميّز بين هذين الشكلين، فينبغي أن تعتمد NSCopying بدلًا من ذلك.

بالنسبة إلى حاسبة البلاطات، الحد العملي واضح: يجب ألا يظل كائن Foundation المُمرر كمدخل مرجعًا قابلًا للتغيير خارجيًا إلى إعدادات يُفترض أنها مستقلة. خذ لقطة مستقلة مناسبة عند الإدخال، وافصل التخزين بالشكل الملائم قبل التعديلات، وأعِد لقطات قيمية بدلًا من الكائن الداخلي القابل للتغيير. تحقّق من كيفية تعامل الفئة المحددة مع أي محتويات متداخلة قابلة للتغيير قبل الاعتماد على عملية النسخ الخاصة بها.

هل تجعل النسخ عند الكتابة النوع قابلًا للإرسال؟

يتطلب استقلال القيم والتوافق مع التزامن استدلالًا منفصلًا. تشترط وثائق Sendable من Apple أن تكون الأعضاء المخزنة في البنية التي تتحقق مطابقة قابلية إرسالها قابلة للإرسال. ولا تحقق فئة تخزين خاصة قابلة للتغيير هذا الشرط لمجرد أن النوع الذي يحتويها بنية.

كما أن فحص التفرد ليس آلية مزامنة. تحذّر Apple من أن isKnownUniquelyReferenced قد يعيد true أثناء الوصول المتزامن، وتشترط مزامنة مناسبة عند استخدامه على هذا النحو. فهو يفحص المراجع القوية، وقد تظل هناك مراجع بديلة ضعيفة أو غير مالكة. لذا فإن إبقاء التخزين غير متاح من خارج الغلاف مهم بقدر أهمية فحص التفرد. راجع القيود الموثقة للدالة في المكتبة القياسية.

أبقِ هذا المثال ضمن سياق حساب متزامن واحد. إذا احتاج إصدار لاحق إلى تمرير الإعدادات بين نطاقات التزامن، فالبديل المباشر هو بنية تحتوي على خصائص قيمية قابلة للإرسال، مع مطابقة Sendable يجري التحقق منها. إن إضافة @unchecked Sendable تنقل مسؤولية صحة التنفيذ إلى منفّذه؛ لكنها لا توفر التنسيق المفقود. توضح Apple هذه المتطلبات في مرجع Sendable.

في حساب صغير على سطح المكتب، فضّل خصائص القيم المباشرة متى لبّت متطلبات المهمة. وعندما يلزم الإبقاء على تخزين فئات قابل للتغيير، فحافظ على العقد عبر الملكية الخاصة ونسخ التخزين بالكامل والتحكم في التعديلات والتحقق من الاستقلال الملحوظ. والنتيجة العملية يمكن التنبؤ بها: يترك تغيير إعدادات العمل في معاينة ما كل قيمة تخطيط محتفَظ بها بنتيجتها المحسوبة الخاصة.

المصادر