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

حدّد بدقة متى تكون بلاطتان متماثلتين

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

اكتب العقد قبل تنفيذ أي من البروتوكولين. في هذا المثال، يتساوى مفتاحان عندما تتساوى أسماؤهما بعد طيّ حالة الأحرف، وعرضاهما وارتفاعاهما. الأبعاد وحدات رسم صحيحة موجبة. وتظل المسافات البادئة واللاحقة ذات دلالة. هذه قاعدة تطبيق توضيحية، وليست متطلبًا تفرضه Swift.

المدخلات الاصطناعية العلاقة المتوقعة السبب
Orbit، 40 × 20 وORBIT، 40 × 20 متساويان لا تؤثر حالة الأحرف
Orbit، 40 × 20 وorbit، 41 × 20 غير متساويين العرض جزء من المساواة
Orbit، 40 × 20 وorbit، 40 × 21 غير متساويين الارتفاع جزء من المساواة
Orbit، 40 × 20 وorbit ، 40 × 20 غير متساويين تُحفظ المسافة اللاحقة

استخدم مفتاحًا مخصصًا عندما تختلف قاعدة تجميع معينة عن القيمة الكاملة للبلاطة المعروضة. احتفظ بطريقة الكتابة الأصلية للاسم في نموذج العرض المحيط. فهذا يجنبك اعتبار وصفين كاملين للبلاطة قابلين للاستبدال بينما تختلف أسماؤهما الظاهرة بوصفها جزءًا من قيمتيهما. يصف توثيق Equatable في Swift المساواة من حيث قابلية الاستبدال، ويشترط الانعكاسية والتماثل والتعدي. Equatable

استخدم المكونات نفسها للمساواة والتجزئة

يرث بروتوكول Hashable في Swift من Equatable. ويتمثل متطلبه الأساسي في أن تمرر المثيلات المتساوية القيم نفسها إلى أداة التجزئة وبالترتيب نفسه. ويمكن استخدام القيم القابلة للتجزئة كعناصر في المجموعات ومفاتيح في القواميس. Hashable

من الأخطاء الشائعة مقارنة الأسماء بعد تطبيعها في ==، مع تجزئة تهجئتها الأصلية. عندها يكون للاسم معنيان مختلفان في الطريقتين. ومن الأخطاء الأخرى إسقاط العرض من المساواة مع تضمينه في التجزئة. يسمح كلا التصميمين للقيم المعلَن أنها متساوية بتقديم مدخلات مختلفة للتجزئة. توجّه Apple صراحةً إلى استخدام الخصائص المهمة نفسها في الطريقتين عند تنفيذ تخصيص مخصص. Adopting Common Protocols

في هذا المثال، ضع مكونات المساواة في بنية متداخلة خاصة. دع Swift يستنتج المساواة والتجزئة لتلك البنية، ثم فوّض إليها العمليتين الخارجيتين. وهكذا تصبح قائمة المكونات تصريحًا واحدًا يمكنك فحصه. تدعم Swift استنتاج المطابقة للبنى التي تتوافق خصائصها المخزنة مع البروتوكولات المطلوبة. Protocols — Adopting a Protocol Using a Synthesized Implementation

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

شغّل المثال المحلي الكامل على Mac

ستحتاج إلى سلسلة أدوات Swift مثبتة مع Foundation. في Terminal، تحقّق منها باستخدام swift --version، كما توصي وثائق البدء باستخدام Swift. إذا لم تكن Swift متاحة، فاتبع دليل التثبيت الرسمي لـ macOS قبل المتابعة. Getting Started، Install Swift — macOS

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

swift - <<'SWIFT'
import Foundation

struct TileKey: Hashable, Sendable {
    private struct Components: Hashable, Sendable {
        let foldedName: String
        let width: Int
        let height: Int
    }

    private let components: Components

    init(name: String, width: Int, height: Int) {
        precondition(width > 0 && height > 0,
                     "Tile dimensions must be positive")

        let foldedName = name.folding(
            options: [.caseInsensitive],
            locale: Locale(identifier: "en_US_POSIX")
        )
        components = Components(
            foldedName: foldedName,
            width: width,
            height: height
        )
    }

    static func == (lhs: TileKey, rhs: TileKey) -> Bool {
        lhs.components == rhs.components
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(components)
    }
}

let a = TileKey(name: "Orbit", width: 40, height: 20)
let b = TileKey(name: "ORBIT", width: 40, height: 20)
let c = TileKey(name: "orbit", width: 40, height: 20)
let wider = TileKey(name: "orbit", width: 41, height: 20)
let taller = TileKey(name: "orbit", width: 40, height: 21)
let spaced = TileKey(name: "orbit ", width: 40, height: 20)
let samples = [a, b, c, wider, taller, spaced]

precondition(a == b && b == c && a == c)
precondition(a != wider && a != taller && a != spaced)

// Check the equality laws over this finite sample.
for first in samples {
    precondition(first == first, "Reflexivity failed")
    for second in samples {
        precondition((first == second) == (second == first),
                     "Symmetry failed")
        if first == second {
            precondition(first.hashValue == second.hashValue,
                         "Equal values have different hashes")
        }
        for third in samples where first == second && second == third {
            precondition(first == third, "Transitivity failed")
        }
    }
}

let uniqueKeys = Set(samples)
precondition(uniqueKeys.count == 4)
precondition(uniqueKeys.contains(b))

let patterns = [a: "striped"]
precondition(patterns[b] == "striped")

print("Unique keys:", uniqueKeys.count)
print("Equivalent key found:", uniqueKeys.contains(b))
print("Pattern:", patterns[b] ?? "missing")
print("Sample contract checks passed")
SWIFT

المخرجات المتوقعة:

Unique keys: 4
Equivalent key found: true
Pattern: striped
Sample contract checks passed

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

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

اجعل تطبيع النص قاعدة صريحة

تستدعي خطوة التطبيع folding(options:locale:) من Foundation عبر واجهة السلاسل النصية في Swift. تعيد الواجهة سلسلة طُبّقت عليها خيارات الطيّ المحددة. هنا، لا يُطلب سوى .caseInsensitive. StringProtocol.folding(options:locale:)

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

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

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

شخّص عدم التطابق دون الاعتماد على أرقام التجزئة

اتبع التسلسل التالي عند تكييف هذا التصميم مع نوع موجود:

  1. اكتب زوجًا واحدًا يجب أن يتساوى، وزوجًا لكل حقل يجب أن يميز القيم.
  2. تتبّع القيم الفعلية التي تقارنها ==، بما فيها التطبيع والخصائص المشتقة.
  3. تتبّع القيم المقدمة إلى hasher.combine، بما فيها ترتيبها.
  4. انقل التعريف المشترك إلى قيمة مكونات واحدة غير قابلة للتغيير، أو اجعل التنفيذين يستخدمان المكونات نفسها صراحةً.
  5. تحقّق من قوانين المساواة، وتجزئات القيم المتساوية، والانتماء إلى المجموعة، والبحث في القاموس باستخدام عينات الاختبار نفسها.

لا تنفّذ المساواة بمقارنة hashValue. يجب أن تكون للقيم المتساوية تجزئات متساوية، لكن تطابق التجزئة لا يثبت المساواة. تظل مكونات المساواة الكاملة هي الأساس لـ ==. وبالمثل، فإن اختبارًا يشترط اختلاف تجزئات كل زوج غير متساوٍ يطلب أكثر مما يضمنه عقد التجزئة. Hashable

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

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

أبقِ متطلبات التزامن منفصلة عن المساواة

يصرّح المثال بأن البنيتين Sendable لأن القيم المخزنة فيهما سلاسل نصية وأعداد صحيحة وبنية أخرى قابلة للإرسال. توثّق Swift أن البنى القابلة للإرسال يجب أن تحتوي على خصائص مخزنة قابلة للإرسال. يتعلق الإرسال بنقل القيم بين نطاقات التزامن؛ ولا يحدد القيم التي تتساوى. Sendable، The Swift Programming Language — Concurrency

لا يحتاج هذا الحساب المحلي إلى مهام أو ممثلين. إذا أصبح لاحقًا جزءًا من كود سطح مكتب متزامن، فأبقِ المساواة والتجزئة عمليتين متزامنتين على مكونات المفتاح المخزنة. وتعامل مع أي مجموعة مشتركة قابلة للتغيير بوصفها مسألة تصميم تزامن منفصلة. يصف دليل اللغة الممثلين والعزل بأنهما آليتان لتنسيق الوصول إلى الحالة القابلة للتغيير. The Swift Programming Language — Concurrency

هناك أيضًا حد فاصل مع Foundation عند تكييف هذا النمط: تستخدم الفئة الفرعية لـ NSObject سلوك المساواة والتجزئة الموروث. ومسار التخصيص المدعوم من Apple هو تجاوز isEqual(_:) وhash معًا. أما البنية العادية في Swift في هذا المثال فتستخدم == وhash(into:). Adopting Common Protocols

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

المصادر