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

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

حدّد معنى «فريد» قبل اختيار مجموعة

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

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

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

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

أنشئ مثال Swift محليًا كاملًا على جهاز Mac

تحتاج إلى تثبيت سلسلة أدوات Swift. شغّل swift --version في Terminal للتأكد من توفر الأمر. يشرح البرنامج التعليمي الرسمي من Swift، إنشاء أداة لسطر الأوامر، هذا التحقق وإنشاء حزمة تنفيذية وتشغيلها.

أنشئ دليلًا جديدًا للمثال وابدأ فيه تهيئة حزمة تنفيذية:

mkdir OrderDemo
cd OrderDemo
swift package init --name OrderDemo --type executable

استبدل ملف المصدر التنفيذي الذي أُنشئ ضمن Sources بالبرنامج الكامل أدناه. قد يختلف اسم الملف الذي أُنشئ وبنية الدليل باختلاف سلسلة الأدوات؛ استبدل نقطة الدخول الموجودة بدلًا من إضافة نقطة دخول ثانية. يستخدم هذا البرنامج نقطة دخول @main، لذا ضعه في ملف اسمه OrderDemo.swift، مع إعادة تسمية main.swift الذي أُنشئ إن لزم الأمر. يشرح البرنامج التعليمي لسطر الأوامر من Swift الاختيار بين ملف main.swift ونقطة دخول @main.

لا حاجة إلى عمليات استيراد أو تبعيات للحزمة. يستخدم البرنامج أنواعًا ودوال من المكتبة القياسية.

struct Tile: Hashable {
    let shape: String
    let size: Int
}

func uniqueFirst<Value, Key: Hashable>(
    _ values: [Value],
    by key: (Value) -> Key
) -> [Value] {
    var seen: Set<Key> = []
    var result: [Value] = []

    for value in values {
        if seen.insert(key(value)).inserted {
            result.append(value)
        }
    }

    return result
}

@main
struct OrderDemo {
    static func main() {
        let input = [
            Tile(shape: "circle", size: 12),
            Tile(shape: "square", size: 8),
            Tile(shape: "circle", size: 12),
            Tile(shape: "circle", size: 20),
            Tile(shape: "square", size: 8)
        ]

        let retained = uniqueFirst(input, by: { $0 })
        let total = retained.reduce(0) { $0 + $1.size }
        let skipped = input.count - retained.count

        let expected = [
            Tile(shape: "circle", size: 12),
            Tile(shape: "square", size: 8),
            Tile(shape: "circle", size: 20)
        ]
        precondition(retained == expected)
        precondition(total == 40)
        precondition(skipped == 2)

        let empty: [Tile] = []
        precondition(uniqueFirst(empty, by: { $0 }).isEmpty)
        precondition(
            uniqueFirst([input[0], input[0]], by: { $0 })
                == [input[0]]
        )

        let byShape = uniqueFirst(input, by: { $0.shape })
        precondition(byShape == [input[0], input[1]])
        let shapeTotal = byShape.reduce(0) { $0 + $1.size }
        precondition(shapeTotal == 20)

        for tile in retained {
            print("\(tile.shape): \(tile.size)")
        }
        print("Total: \(total)")
        print("Skipped duplicates: \(skipped)")
        print("Shape-only total: \(shapeTotal)")
    }
}

شغّل الأمر التالي من دليل الحزمة:

swift run OrderDemo

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

circle: 12
square: 8
circle: 20
Total: 40
Skipped duplicates: 2
Shape-only total: 20

لماذا يحافظ أسلوب المصفوفة والمجموعة على الترتيب؟

المصفوفة هي المصدر الوحيد لترتيب الإخراج. تحلّق الحلقة على كل موضع إدخال بالتسلسل، وتضع append(_:) القيمة المقبولة في نهاية النتيجة. ولا تُستخدم المجموعة إلا لمعرفة ما إذا كان المفتاح المحدد قد ظهر من قبل؛ فالبرنامج لا يمرّ على عناصرها.

والتعبير المحوري هو seen.insert(key(value)).inserted. توثّق Swift أن insert(_:) تعيد inserted بوصفها true عند إضافة عضو جديد، وتعيد false إذا كان هناك عضو مساوٍ موجود بالفعل. ويبقى العضو الموجود كما هو. تتيح هذه القيمة المعادة لعملية واحدة أن تقرر ما إذا كانت ستضيف القيمة الأصلية. راجع توثيق إدراج العناصر في Set من Apple.

بالنسبة إلى الإدخال المقدم، تُقبل الدائرة والمربع الأولان. وتُتخطى الدائرة الثانية المطابقة. وتُقبل الدائرة الأكبر لأن قيمتها الكاملة مختلفة، بينما يُتخطى المربع المكرر. لذا تكون الأحجام المحتفَظ بها هي 12 + 8 + 20 = 40.

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

كيف تعبّر قيم Swift المخصصة عن المساواة؟

يطلب التصريح struct Tile: Hashable مطابقةً مولّدة تلقائيًا. والخاصيتان المخزنتان، String وInt، تدعمان التجزئة، لذا يمكن لـ Swift توليد السلوك المطلوب. توثّق Apple شروط هذا التوليد في صفحة Hashable.

يجعل استخدام { $0 } مفتاحًا قيمة Tile كاملة هي التي تحدد التفرد. أما استخدام { $0.shape } فيغيّر القاعدة بحيث يُحتفَظ بعنصر واحد لكل شكل. في المثال، يؤدي ذلك إلى إزالة الدائرة اللاحقة ذات الحجم 20، ما يخفض المجموع إلى 20. هذه مقارنة مقصودة بين قاعدتين، وليست تنفيذًا ثانيًا للقاعدة نفسها.

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

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

متى تستخدم مجموعة مرتبة بدلًا من ذلك؟

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

الخيار الحالة المناسبة الحد ذو الصلة
مصفوفة مع مجموعة تمريرة واحدة على قيم Swift الاصطناعية، مع احتمال استخدام مفتاح محدد تحدد دالتك سلوك الظهور الأول
OrderedSet من Swift Collections استخدام مستمر لمجموعة مرتبة من قيم فريدة قابلة للتجزئة يتطلب حزمة Swift Collections منفصلة
NSOrderedSet من Foundation عندما تعمل واجهة برمجة التطبيقات أصلًا مع مجموعات كائنات مرتبة من Foundation فئة تمثل مجموعة مرتبة ثابتة؛ وتستخدم العمليات القابلة للتعديل NSMutableOrderedSet

يوضح توثيق Swift Collections الرسمي أن OrderedSet مجموعة مرتبة من عناصر فريدة، ويُستورد عبر OrderedCollections. ويوفر عمليات تراعي المواضع مثل append(_:) وinsert(_:at:). هذه واجهات برمجة للحزمة، لذلك فإن إضافة import OrderedCollections وحده لا يهيئ مشروعًا لا يعتمد على تبعيات. راجع OrderedSet.

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

ما الذي يتغير عندما يغذي الحساب واجهة سطح مكتب؟

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

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

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

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

تحقّق من القاعدة قبل إعادة استخدام الدالة

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

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

بالنسبة إلى قائمة الأشكال الاصطناعية المقدمة، تتكون النتيجة النهائية من ثلاث قيم بترتيب ظهورها الأول، ويبلغ مجموعها 40. اجعل المصفوفة مسؤولة عن الترتيب، والمجموعة مسؤولة عن العضوية، والمفتاح المحدد مسؤولًا عن معنى «فريد».

المصادر