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

لماذا يغيّر Codable المُولَّد تلقائيًا التمثيل؟

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

لا يوفر المطابقة التلقائية التمثيل المطلوب. يضع تصميم Swift المُنفَّذ للتوليد التلقائي للتعدادات اسم الحالة في حاوية مفاتيح خارجية، ثم يضع القيم المرتبطة داخل حاوية أخرى. وتُمنح القيم المرتبطة غير المسماة مفاتيح مُولَّدة مثل _0. لذلك تكتسب حالة السلسلة أغلفة بنيوية بدلًا من أن تصبح سلسلة عادية. توثّق هذه القواعد في التوليد التلقائي لـ Codable للتعدادات ذات القيم المرتبطة.

يمكن لتغيير CodingKeys إعادة تسمية تلك الأغلفة، لكن الإبقاء على التوليد التلقائي يعني الإبقاء على نموذجه البنيوي. والحل المدعوم هو تنفيذ init(from:) وencode(to:) بنفسك. توثّق Apple صراحةً المطابقة اليدوية للأنواع التي تختلف بنيتها في Swift عن بنيتها المُرمَّزة في ترميز الأنواع المخصصة وفك ترميزها.

العودية والتمثيل قراران منفصلان. يتيح المعدِّل indirect في Swift تخزين التعدادات العودية؛ أما أساليب الترميز المخصصة فتحدد شكل البيانات المُرمَّزة. يشرح دليل اللغة وضع indirect قبل تعريف التعداد لتطبيق الإحالة غير المباشرة على حالاته ذات القيم المرتبطة في التعدادات.

ما الذي ينبغي للتمثيل العادي أن يحافظ عليه تحديدًا؟

استخدم العقد التالي لهذا المثال. وهو يدعم عمدًا مجموعة صغيرة من القيم المناسبة لمعاينة تخطيط اصطناعية. تسمح قواعد التحويل الموثقة في Foundation بالسلاسل والأعداد والمصفوفات والقواميس وكائنات null، وتشترط أن تكون مفاتيح القواميس سلاسل، وتستبعد الأعداد غير المنتهية. راجع JSONSerialization.

حالة التعداد القيمة العادية متطلب التوافق
null NSNull الحفاظ على قيمة null صريحة
bool قيمة منطقية تمييزها عن الصفر والواحد العدديين
number قيمة Double منتهية قبول حدود دقة Double
string سلسلة الحفاظ على النص من دون تحويله إلى عدد
array مصفوفة من القيم العادية الحفاظ على ترتيب العناصر والمصفوفات الفارغة
object قاموس مفاتيحه سلاسل الحفاظ على المفاتيح والقيم المتداخلة والكائنات الفارغة

يعني التوافق هنا الحفاظ على هذه الفئات وقيمها ضمن الحدود العددية المذكورة. ولا يتطلب الحفاظ على فئة فرعية معينة من أعداد Foundation أو على ترتيب ظهور مفاتيح القاموس. ويجب أيضًا أن يبقى إدخال null صريحًا: فالكائن الذي يحتوي على "subtitle": NSNull() له معنى مختلف في النموذج عن كائن لا يحتوي على المفتاح subtitle.

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

كيف تنفّذ التعداد العودي؟

استخدم بيئة Swift حديثة على Mac يتوفر فيها Foundation. تشكّل الكتل البرمجية الثلاث التالية، بالترتيب، مثالًا كاملًا على المستوى الأعلى: التعداد ودوال التحويل وروتين تحقق قابل للتنفيذ. تُنشأ جميع قيم العينة في الذاكرة. Swift غير متاح في بيئة البحث، لذا جرت مراجعة المثال من دون تنفيذه.

يتحقق مفكك الترميز أولًا من null، ثم يحاول قراءة الأنواع البدائية المدعومة. توفر SingleValueDecodingContainer من Apple هذه العمليات، ومنها decodeNil(). ولا تعتبر الدالة المساعدة المحلية عدم تطابق النوع سببًا لتجربة نوع بدائي آخر إلا في هذه الحالة؛ أما أخطاء فك الترميز الأخرى فتنتشر إلى المستدعي.

import Foundation

indirect enum PlainValue: Codable, Equatable {
    case null
    case bool(Bool)
    case number(Double)
    case string(String)
    case array([PlainValue])
    case object([String: PlainValue])

    init(from decoder: Decoder) throws {
        let scalar = try decoder.singleValueContainer()

        if scalar.decodeNil() {
            self = .null
            return
        }

        func matching<T: Decodable>(_ type: T.Type) throws -> T? {
            do {
                return try scalar.decode(type)
            } catch DecodingError.typeMismatch(_, _) {
                return nil
            }
        }

        if let value = try matching(Bool.self) {
            self = .bool(value)
            return
        }
        if let value = try matching(String.self) {
            self = .string(value)
            return
        }
        if let value = try matching(Double.self) {
            guard value.isFinite else {
                throw DecodingError.dataCorrupted(.init(
                    codingPath: decoder.codingPath,
                    debugDescription: "Expected a finite number."
                ))
            }
            self = .number(value)
            return
        }

        var sequence: UnkeyedDecodingContainer?
        do {
            sequence = try decoder.unkeyedContainer()
        } catch DecodingError.typeMismatch(_, _) {
            sequence = nil
        }

        if var items = sequence {
            var values: [PlainValue] = []
            while !items.isAtEnd {
                values.append(try items.decode(PlainValue.self))
            }
            self = .array(values)
            return
        }

        self = .object(try scalar.decode([String: PlainValue].self))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        switch self {
        case .null:
            try container.encodeNil()
        case .bool(let value):
            try container.encode(value)
        case .number(let value):
            guard value.isFinite else {
                throw EncodingError.invalidValue(value, .init(
                    codingPath: encoder.codingPath,
                    debugDescription: "Expected a finite number."
                ))
            }
            try container.encode(value)
        case .string(let value):
            try container.encode(value)
        case .array(let values):
            try container.encode(values)
        case .object(let values):
            try container.encode(values)
        }
    }
}

يكتب كل فرع من فروع الترميز محتواه مباشرةً. ويستدعي فرع null صراحةً encodeNil()، بينما تفوّض فروع المجموعات التنفيذ إلى آليات الترميز الخاصة بالمصفوفات والقواميس. توثّق Apple عمليات الترميز البدائية والعامة في SingleValueEncodingContainer، ومطابقة المجموعات في ترميز الأنواع المخصصة وفك ترميزها.

يُفصل عمدًا التحقق من المصفوفة عن فك ترميز عناصرها. فقد يؤدي طلب حاوية غير مفهرسة إلى رمي خطأ عدم تطابق النوع عندما لا تكون القيمة الحالية مصفوفة. وبعد التعرّف على المصفوفة، يُفك ترميز كل عنصر باستخدام try المعتاد، لذا ينتشر فشل عنصر بدلًا من أن يؤدي إلى محاولة غير ذات صلة لفك ترميز كائن. يرد وصف السلوك الموثق في unkeyedContainer() وUnkeyedDecodingContainer.

يستهدف هذا التنفيذ مسار ترميز JSON في Foundation ومجموعة القيم المذكورة. اختبر أي مُرمِّز أو مفكك ترميز آخر وفق العقد نفسه قبل استخدامه؛ فالمطابقة مع Codable وحدها لا تثبت تطابق السلوك عبر تنسيقات التمثيل.

كيف تحوّل القيم إلى كائنات Foundation بالكامل في الذاكرة؟

توفر Foundation نقطتي تحويل مفيدتين: تنتج JSONEncoder قيمة Data من قيمة قابلة للترميز، ويعيد JSONDecoder بناء قيمة قابلة لفك الترميز من Data. وتحول JSONSerialization بين هذا التمثيل الوسيط وكائنات Foundation. ويتيح الجمع بين هذه العمليات الموثقة جسر التحويل التالي في الذاكرة. راجع JSONEncoder وJSONDecoder وJSONSerialization.

func foundationObject(from value: PlainValue) throws -> Any {
    let bytes = try JSONEncoder().encode(value)
    return try JSONSerialization.jsonObject(
        with: bytes,
        options: [.fragmentsAllowed]
    )
}

func plainValue(from object: Any) throws -> PlainValue {
    let bytes = try JSONSerialization.data(
        withJSONObject: object,
        options: [.fragmentsAllowed]
    )
    return try JSONDecoder().decode(PlainValue.self, from: bytes)
}

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

خيارا .fragmentsAllowed مهمان لأن القيمة العودية قد تكون قيمة جذرية مفردة. فالسلسلة أو القيمة المنطقية أو العدد أو null في المستوى الأعلى تحتاج إلى المعالجة نفسها التي تحتاج إليها القيمة المتداخلة. توثّق Foundation هذا السلوك على نحو منفصل في ReadingOptions.fragmentsAllowed وWritingOptions.fragmentsAllowed.

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

ما الفحوص التي تثبت التوافق؟

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

func runExample() throws {
    let plain: [String: Any] = [
        "title": "Tile preview",
        "enabled": true,
        "spacing": 8.5,
        "subtitle": NSNull(),
        "tiles": [
            ["label": "Circle", "size": 24.0],
            ["label": "Square", "size": 32.0]
        ]
    ]

    let expected: PlainValue = .object([
        "title": .string("Tile preview"),
        "enabled": .bool(true),
        "spacing": .number(8.5),
        "subtitle": .null,
        "tiles": .array([
            .object([
                "label": .string("Circle"),
                "size": .number(24)
            ]),
            .object([
                "label": .string("Square"),
                "size": .number(32)
            ])
        ])
    ])

    let model = try plainValue(from: plain)
    precondition(model == expected)

    let output = try foundationObject(from: model)
    guard let object = output as? [String: Any] else {
        preconditionFailure("Expected a plain object root.")
    }
    precondition(Set(object.keys) == Set(plain.keys))
    precondition(object["title"] as? String == "Tile preview")
    precondition(object["subtitle"] is NSNull)
    precondition((object["tiles"] as? [Any])?.count == 2)

    let restored = try plainValue(from: output)
    precondition(restored == expected)

    let roots: [PlainValue] = [
        .null, .bool(false), .bool(true),
        .number(0), .number(1), .number(8.5),
        .string("1"), .array([]), .object([:]),
        expected
    ]
    for root in roots {
        let graph = try foundationObject(from: root)
        let decoded = try plainValue(from: graph)
        precondition(decoded == root)
    }

    do {
        _ = try foundationObject(from: .number(.infinity))
        preconditionFailure("Expected nonfinite number rejection.")
    } catch EncodingError.invalidValue(_, _) {
        // Expected: this model requires finite numbers.
    }

    print("All compatibility checks passed.")
}

try runExample()

رسالة النجاح المتوقعة هي All compatibility checks passed. يكشف فحص مفاتيح الجذر إضافة غلاف خارجي للحالة. ويكشف النموذج المتوقع المستقل اختيار الحالة الخاطئة، بما في ذلك الخلط بين قيمة منطقية وعدد، وبين نص يشبه العدد وعدد. يؤكد فحص null بقاء subtitle، بينما تغطي فحوص الجذر القيم المفردة وتميّز المصفوفات الفارغة عن الكائنات الفارغة.

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

ما حدود هذا النهج؟

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

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

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

المصادر