أنشئ مكرّرًا واحدًا قابلًا للتغيير واستدعِ await iterator.next() لكل قيمة متتالية. ولإجراء حساب صغير لسطر الأوامر على Mac، أبقِ كلًا من التدفق والمكرّر خارج عملية القراءة المتكررة، واعتبر nil إشارة إلى اكتمال القراءة.

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

ما الذي يُعاد تشغيله فعليًا عندما تتكرر القيم؟

تفصل Swift بين التسلسل غير المتزامن والمكرّر الخاص به. يوفّر التسلسل مكرّرًا عبر makeAsyncIterator()، ويتقدم المكرّر من خلال استدعاءات next(). يعيد كل استدعاء عنصرًا أو nil عند انتهاء التكرار. توثّق Apple هذا العقد في AsyncIteratorProtocol.

هناك عمليتان مختلفتان ينبغي فحصهما عندما يتلقى التطبيق القيمة الأولى مرارًا: إنشاء مكرّر آخر وإنشاء تدفق آخر. وهما غير قابلتين للتبادل. في التنفيذ الحالي للمكتبة القياسية، يحتفظ مكرّر AsyncStream بسياق التدفق المشترك، ويمرّر makeAsyncIterator() هذا السياق الموجود إلى المكرّر. لذا فإن إنشاء مكرّر آخر للتدفق نفسه لا ينشئ إعادة تشغيل مستقلة من البداية. راجع التنفيذ الرسمي لـ AsyncStream.

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

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

نفّذ حسابًا محليًا كاملًا على Mac

استخدم Mac مع سلسلة أدوات Swift 6 أو أحدث. في Terminal، تحقّق من إصدار المترجم المثبّت، ثم أنشئ حزمة تنفيذية صغيرة في دليل جديد. يوثّق دليل Swift.org إنشاء أداة لسطر الأوامر التحقق من الإصدار وإنشاء الحزمة وسير عمل swift run.

swift --version
mkdir IteratorDemo
cd IteratorDemo
swift package init --name IteratorDemo --type executable

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

لا تحتاج إلى استيراد أي شيء في هذا المثال: AsyncStream جزء من Swift، بينما Foundation إطار عمل منفصل. ولا يحتاج الحساب إلى أي واجهة برمجية من Foundation. تظهر هذه الحدود في توثيق AsyncStream من Apple ونظرة عامة على Foundation.

@main
struct IteratorDemo {
    static func makeValues() -> AsyncStream<Int> {
        AsyncStream<Int>(bufferingPolicy: .bufferingOldest(3)) {
            continuation in
            for value in [4, 7, 10] {
                let result = continuation.yield(value)
                guard case .enqueued = result else {
                    preconditionFailure("A sample value was not enqueued")
                }
            }
            continuation.finish()
        }
    }

    static func main() async {
        let stream = makeValues()
        var iterator = stream.makeAsyncIterator()
        var received: [Int] = []
        var squareTotal = 0

        for readNumber in 1...3 {
            guard let value = await iterator.next() else {
                preconditionFailure("Stream ended before read \(readNumber)")
            }

            received.append(value)
            squareTotal += value * value
            print("Read \(readNumber): \(value)")
        }

        let end = await iterator.next()
        let stillEnded = await iterator.next()

        precondition(received == [4, 7, 10])
        precondition(squareTotal == 165)
        precondition(end == nil)
        precondition(stillEnded == nil)

        print("Square total: \(squareTotal)")
        print("Finished: \(end == nil)")
    }
}

شغّل الملف التنفيذي من دليل الحزمة:

swift run IteratorDemo

المخرجات المتوقعة للتطبيق، بعد أي رسائل إنشاء، هي:

Read 1: 4
Read 2: 7
Read 3: 10
Square total: 165
Finished: true

الحساب توضيحي صراحةً: 4 × 4 + 7 × 7 + 10 × 10 = 165. تتحقق التأكيدات من القيم الدقيقة وترتيبها والحساب والسلوك عند الانتهاء. لم يكن مترجم Swift متاحًا في بيئة البحث، لذا جرى فحص هذا البرنامج، لكن لم يُنفّذ هنا.

لماذا يواصل هذا المكرّر عمله على نحو صحيح؟

يستدعي البرنامج makeValues() مرة واحدة وmakeAsyncIterator() مرة واحدة. وتستخدم قراءاته الثلاث كلها المتغير القابل للتغيير نفسه. تعرّف Apple الطريقة next() الخاصة بالمكرّر بأنها تعديلية وغير متزامنة، ما يفسر استخدام كل من var iterator وawait iterator.next().

يصدر المنتج قيمه الثلاث أثناء إنشاء التدفق، قبل بدء الاستهلاك. لذا فإن سعة التخزين المؤقت جزء من عقد المثال: تتسع .bufferingOldest(3) للأعداد الصحيحة الثلاثة كلها. ويُظهر فحص نتيجة كل yield أي إخفاق غير متوقع في إضافة العناصر إلى قائمة الانتظار، بدلًا من السماح للحساب بالاستمرار مع قيم مفقودة. توثّق Apple أن yield(_:) تحاول تخزين قيمة مؤقتًا عند عدم وجود قراءة تنتظرها.

يؤدي استدعاء finish() بعد عمليات الإرسال إلى إغلاق الإنتاج مع السماح باستهلاك القيم المخزنة مؤقتًا. ينص اقتراح Swift المقبول صراحةً على وصول العناصر المخزنة مؤقتًا إلى المستهلك قبل القيمة النهائية nil. لهذا تنجح القراءات الثلاث الأولى رغم أن المنتج قد انتهى بالفعل. راجع AsyncStream وAsyncThrowingStream، SE-0314.

تعثر القراءة الرابعة على تدفق منتهٍ ومستنفد، وتعيد nil. وتؤكد القراءة الخامسة العقد النهائي: بعد أن يعيد المكرّر nil، يجب أن تعيد القراءات اللاحقة أيضًا nil. لا تفسّر nil على أنه تعليمات لإعادة المحاولة لاحقًا أو إنشاء مكرّر جديد؛ إذ يعرّف AsyncIteratorProtocol هذه القيمة بأنها نهاية التكرار.

اختر بين القراءات الصريحة والحلقة المتواصلة

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

عندما تكون المهمة ببساطة معالجة كل عنصر حتى الاكتمال، استخدم حلقة for await واحدة. يعرض ملخص AsyncStream من Apple صيغة الاستهلاك المدعومة هذه. في البرنامج النموذجي، المقطع التالي بديل للدالة main() الحالية بأكملها؛ احتفظ بالدالة makeValues() الحالية والنوع المحيط بها:

static func main() async {
    let stream = makeValues()
    var received: [Int] = []
    var squareTotal = 0

    for await value in stream {
        received.append(value)
        squareTotal += value * value
    }

    precondition(received == [4, 7, 10])
    precondition(squareTotal == 165)
    print("Square total: \(squareTotal)")
}

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

شخّص القراءات المتكررة أو المفقودة أو غير المكتملة

يفصل التسلسل التالي مشكلات عمر المصدر عن مشكلات التخزين المؤقت والاكتمال. طبّقه على المثال الثابت قبل إضافة مزيد من السلوك.

  1. تكرار 4: افحص إنشاء المصدر. تحقّق مما إذا كانت كل عملية تستدعي makeValues() مجددًا، أو تصل إلى أداة الحصول على قيمة تنشئ تدفقًا جديدًا. أبقِ استدعاء دالة المصنع خارج عمليات القراءة المتكررة.
  2. قيم غير متوقعة بين المستهلكين: افحص الملكية. خصص لهذه العملية مستهلكًا واحدًا ومكرّرًا واحدًا. لا توفر المكرّرات المتعددة للتدفق نفسه نسخًا مستقلة من قيمه.
  3. قيم مفقودة: افحص سعة التخزين المؤقت. ينتج المثال الأعداد الصحيحة الثلاثة كلها قبل الاستهلاك. وتغيّر سعة أصغر القيم التي تظل متاحة.
  4. قراءة أخيرة لا تكتمل أبدًا: افحص الاكتمال. في تدفق قائم على continuation، لا يؤدي فراغ المخزن المؤقت وحده إلى إنهاء الإنتاج. تأكد من أن المنتج المحدود يستدعي finish().
  5. قراءات متداخلة: نظّم الاستهلاك تسلسليًا. انتظر اكتمال قراءة قبل بدء قراءة أخرى على المكرّر.

تغييرات التخزين المؤقت لها عواقب متوقعة في ظروف هذا المثال. مع .bufferingOldest(2)، تبقى القيمتان الأوليان ويُسقط العنصر الجديد 10. ومع .bufferingNewest(2)، تُهمَل أقدم قيمة مخزنة مؤقتًا، وهي 4، وتبقى 7 و10. وهذه استنتاجات من ترتيب الإنتاج المذكور استنادًا إلى سياستي Apple الموثقتين bufferingOldest(_:) وbufferingNewest(_:).

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

أبقِ الوصول إلى المكرّر تسلسليًا. تذكر Apple أن AsyncStream.Iterator لا يطابق Sendable، وأن استدعاءات next() المتزامنة المتنافسة خطأ برمجي. ويؤثر الإلغاء أيضًا في continuation: إذا أُلغيت مهمة الاستهلاك أثناء انتظار قيمة، يذكر توثيق next() من Apple أن التدفق ينتهي. وإنشاء مكرّر آخر ليس طريقة موثقة لاستئناف ذلك التدفق الملغى.

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

المصادر