التزامن المتقدم في Swift مع HoneyBee

نشرت: 2022-03-11

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

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

في عام 2014 ، قدمت شركة Apple Grand Central Dispatch (GCD) كخطوة دراماتيكية للأمام في التعبير عن العمليات المتزامنة. وفرت GCD ، جنبًا إلى جنب مع كتل ميزات اللغة الجديدة التي رافقتها وتشغيلها ، طريقة لوصف معالج الاستجابة غير المتزامن مباشرة بعد بدء الطلب غير المتزامن. لم يعد يتم تشجيع المبرمجين على نشر تعريف المهام المتزامنة عبر ملفات متعددة في العديد من الفئات الفرعية لـ NSOperation. الآن ، يمكن كتابة خوارزمية كاملة متزامنة بطريقة واحدة. كانت هذه الزيادة في التعبير وسلامة النوع بمثابة تحول مفاهيمي مهم إلى الأمام. قد تبدو الخوارزمية النموذجية لطريقة الكتابة هذه كما يلي:

 func processImageData(completion: (result: Image?, error: Error?) -> Void) { loadWebResource("dataprofile.txt") { (dataResource, error) in guard let dataResource = dataResource else { completion(nil, error) return } loadWebResource("imagedata.dat") { (imageResource, error) in guard let imageResource = imageResource else { completion(nil, error) return } decodeImage(dataResource, imageResource) { (imageTmp, error) in guard let imageTmp = imageTmp else { completion(nil, error) return } dewarpAndCleanupImage(imageTmp) { imageResult in guard let imageResult = imageResult else { completion(nil, error) return } completion(imageResult, nil) } } } } }

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

ربما يبدو شكل كتلة التعليمات البرمجية أعلاه مألوفًا لمعظم مطوري Swift. لكن ما الخطأ في هذا النهج؟ من المحتمل أن تكون القائمة التالية من نقاط الألم مألوفة بنفس القدر.

  • يمكن أن يصبح شكل "هرم العذاب" من كتل التعليمات البرمجية المتداخلة سريعًا غير عملي. ماذا يحدث إذا أضفنا عمليتين غير متزامنتين؟ أربعة؟ ماذا عن العمليات الشرطية؟ ماذا عن سلوك إعادة المحاولة أو الحماية لحدود الموارد؟ رمز العالم الحقيقي ليس أبدًا نظيفًا وبسيطًا مثل الأمثلة في مشاركات المدونة. يمكن أن يؤدي تأثير "هرم العذاب" بسهولة إلى تعليمات برمجية يصعب قراءتها وصعوبة الحفاظ عليها وعرضة للأخطاء.
  • محاولة معالجة الأخطاء في المثال أعلاه ، على الرغم من Swifty ، هي في الواقع غير مكتملة. افترض المبرمج أن كتل رد الاتصال غير المتزامنة بنمط Objective-C ذات المعلمتين ستوفر دائمًا أحد المعلمتين ؛ لن يكون كلاهما معدومًا في نفس الوقت. هذا ليس افتراض آمن. تشتهر الخوارزميات المتزامنة بصعوبة كتابتها وتصحيحها ، والافتراضات التي لا أساس لها هي جزء من السبب. تعتبر معالجة الأخطاء بشكل كامل وصحيح ضرورة لا مفر منها لأي خوارزمية متزامنة تنوي العمل في العالم الحقيقي.
  • بأخذ هذه الفكرة إلى أبعد من ذلك ، ربما لم يكن المبرمج الذي كتب الوظائف غير المتزامنة المسماة مبدئيًا مثلك. ماذا لو كانت هناك ظروف لا يمكن بموجبها استدعاء الوظائف مرة أخرى؟ أو معاودة الاتصال أكثر من مرة؟ ماذا يحدث لصحة processImageData في ظل هذه الظروف؟ المحترفون لا يجازفون. يجب أن تكون الوظائف الحيوية للمهمة صحيحة حتى عندما تعتمد على وظائف مكتوبة من قبل أطراف ثالثة.
  • ربما يكون الأمر الأكثر إقناعًا هو أن الخوارزمية التي تعتبر غير متزامنة مبنية بشكل غير مثالي. أول عمليتين غير متزامنتين عبارة عن عمليات تنزيل للموارد البعيدة. على الرغم من عدم وجود ترابط بينهما ، إلا أن الخوارزمية المذكورة أعلاه تنفذ التنزيلات بالتتابع وليس بالتوازي. وأسباب ذلك واضحة؛ تشجع بنية الكتلة المتداخلة مثل هذا الإسراف. الأسواق التنافسية لا تتسامح مع التباطؤ الذي لا داعي له. إذا لم ينفذ تطبيقك عملياته غير المتزامنة بأسرع ما يمكن ، فسيقوم تطبيق آخر بذلك.

كيف يمكننا أن نفعل أفضل؟ HoneyBee هي مكتبة مستقبلية / وعود تجعل برمجة Swift المتزامنة سهلة ومعبرة وآمنة. دعنا نعيد كتابة الخوارزمية غير المتزامنة أعلاه باستخدام HoneyBee ونفحص النتيجة:

 func processImageData(completion: (result: Image?, error: Error?) -> Void) { HoneyBee.start() .setErrorHandler { completion(nil, $0) } .branch { stem in stem.chain(loadWebResource =<< "dataprofile.txt") + stem.chain(loadWebResource =<< "imagedata.dat") } .chain(decodeImage) .chain(dewarpAndCleanupImage) .chain { completion($0, nil) } }

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

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

تم التعامل مع جميع أخطاء وقت التشغيل المحتملة بشكل كامل. كل توقيع وظيفة يدعمها HoneyBee (هناك 38 منها) مضمون ليتم التعامل معها بشكل كامل. في مثالنا ، ستؤدي عملية رد النداء المكونة من معلمتين من نمط Objective-C إما إلى حدوث خطأ غير صفري سيتم توجيهه إلى معالج الخطأ ، أو ستنتج قيمة غير صفرية والتي ستتقدم إلى أسفل السلسلة ، أو إذا كان كلاهما القيم هي لا شيء ، سوف ينتج عن HoneyBee خطأ يشرح أن وظيفة رد الاتصال لا تفي بعقدها.

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

نأمل أن يكون من الواضح بالفعل أن هذا الشكل من processImageData يوازي تنزيلات الموارد بشكل صحيح لتوفير الأداء الأمثل. أحد أقوى أهداف تصميم HoneyBee هو أن الوصفة يجب أن تبدو مثل الخوارزمية التي تعبر عنها.

أفضل بكثير. حق؟ لكن لدى HoneyBee الكثير لتقدمه.

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

 func export(_ mediaRef: String, completion: @escaping (Media?, Error?) -> Void) { // transcoding stuff completion(Media(context: managedObjectContext), nil) }

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

 func upload(_ media: Media, completion: @escaping (Error?) -> Void) { // network stuff completion(nil) }

نظرًا لأنه يُسمح للمستخدم بتحديد عشرات من عناصر الوسائط في وقت واحد ، فقد حدد مصمم UX قدرًا قويًا من التعليقات حول تقدم التحميل. تم تقطير المتطلبات في الوظائف الأربع التالية:

 /// Called if anything goes wrong in the upload func errorHandler(_ error: Error) { // do the right thing } /// Called once per mediaRef, after either a successful or unsuccessful upload func singleUploadCompletion(_ mediaRef: String) { // update a progress indicator } /// Called once per successful upload func singleUploadSuccess(_ media: Media) { // do celebratory things } /// Called if the entire batch was considered to be uploaded successfully. func totalProcessSuccess() { // declare victory }

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

بالطبع ، يريد Business أن يتم تحميل الدُفعات بأسرع ما يمكن ، لذا فإن التحميل التسلسلي غير وارد. يجب أن يتم التحميل بالتوازي.

ولكن ليس كثيرا. إذا قمت بفصل الدفعة بالكامل بشكل عشوائي ، فإن العشرات من التحميلات async ستغرق NIC (بطاقة واجهة الشبكة) للجوال ، وستستمر عمليات التحميل فعليًا بشكل أبطأ من التسلسل ، وليس أسرع.

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

يجب ألا تتضمن سياسة إعادة المحاولة عملية التصدير لأنها لا تخضع لإخفاقات عابرة.

تكون عملية التصدير مرتبطة بالحساب ولذلك يجب إجراؤها من السلسلة الرئيسية.

نظرًا لأن التصدير مرتبط بالحساب ، يجب أن يحتوي على عدد من المثيلات المتزامنة أقل من عملية التحميل المتبقية لتجنب تعطل المعالج.

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

الوسائط هي كائن NSManagedObject ، والذي يأتي من NSManagedObjectContext وله متطلبات ترابط خاصة به والتي يجب احترامها.

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

 /// An enum describing specific problems that the algorithm might encounter. enum UploadingError : Error { case invalidResponse case tooManyFailures } /// A semaphore to prevent flooding the NIC let outerLimit = DispatchSemaphore(value: 4) /// A semaphore to prevent thrashing the processor let exportLimit = DispatchSemaphore(value: 1) /// The number of times to retry the upload if it fails let uploadRetries = 1 /// Dispatch group to keep track of when the entire process is finished let fullProcessDispatchGroup = DispatchGroup() /// How many of the uploads fully completed. var uploadSuccesses = 0 // this notify block is called when the full process has completed. fullProcessDispatchGroup.notify(queue: DispatchQueue.main) { let successRate = Float(uploadSuccesses) / Float(mediaReferences.count) if successRate > 0.5 { totalProcessSuccess() } else { errorHandler(UploadingError.tooManyFailures) } } // start in the background DispatchQueue.global().async { for mediaRef in mediaReferences { // alert the group that we're starting a process fullProcessDispatchGroup.enter() // wait until it's safe to start uploading outerLimit.wait() /// common cleanup operations needed later func finalizeMediaRef() { singleUploadCompletion(mediaRef) fullProcessDispatchGroup.leave() outerLimit.signal() } // wait until it's safe to start exporting exportLimit.wait() export(mediaRef) { (media, error) in // allow another export to begin exportLimit.signal() if let error = error { DispatchQueue.main.async { errorHandler(error) finalizeMediaRef() } } else { guard let media = media else { DispatchQueue.main.async { errorHandler(UploadingError.invalidResponse) finalizeMediaRef() } return } // the export was successful var uploadAttempts = 0 /// define the upload process and its retry behavior func doUpload() { // respect Media's threading requirements managedObjectContext.perform { upload(media) { error in if let error = error { if uploadAttempts < uploadRetries { uploadAttempts += 1 doUpload() // retry } else { DispatchQueue.main.async { // too many upload failures errorHandler(error) finalizeMediaRef() } } } else { DispatchQueue.main.async { uploadSuccesses += 1 singleUploadSuccess(media) finalizeMediaRef() } } } } } // kick off the first upload doUpload() } } } }

واو! بدون تعليقات ، هذا يعني حوالي 75 سطرًا. هل اتبعت المنطق طوال الطريق؟ ما هو شعورك إذا واجهت هذا الوحش في الأسبوع الأول لك في وظيفة جديدة؟ هل تشعر أنك مستعد للحفاظ عليه ، أو تعديله؟ هل تعرف ما إذا كانت تحتوي على أخطاء؟ هل يحتوي على أخطاء؟

الآن ، فكر في بديل عسل النحل:

 HoneyBee.start(on: DispatchQueue.main) .setErrorHandler(errorHandler) .insert(mediaReferences) .setBlockPerformer(DispatchQueue.global()) .each(limit: 4, acceptableFailure: .ratio(0.5)) { elem in elem.finally { link in link.setBlockPerformer(DispatchQueue.main) .chain(singleUploadCompletion) } .limit(1) { link in link.chain(export) } .setBlockPerformer(managedObjectContext) .retry(1) { link in link.chain(upload) // subject to transient failure } .setBlockPerformer(DispatchQueue.main) .chain(singleUploadSuccess) } .setBlockPerformer(DispatchQueue.main) .drop() .chain(totalProcessSuccess)

كيف تصدمك هذه الصيغة؟ دعونا نعمل من خلاله قطعة قطعة. في السطر الأول ، نبدأ وصفة نحل العسل ، بدءًا من الخيط الرئيسي. بالبدء في الخيط الرئيسي ، نضمن تمرير جميع الأخطاء إلى errorHandler (السطر 2) في الخيط الرئيسي. يقوم السطر 3 بإدراج صفيف mediaReferences في سلسلة العملية. بعد ذلك ، ننتقل إلى قائمة انتظار الخلفية العالمية استعدادًا لبعض التوازي. في السطر الخامس ، نبدأ تكرارًا متوازيًا على كل من mediaReferences . نقصر هذا التوازي على 4 عمليات متزامنة كحد أقصى. نعلن أيضًا أن التكرار الكامل سيعتبر ناجحًا إذا نجح نصف السلاسل الفرعية على الأقل (لا تخطئ). يعلن السطر 6 عن ارتباط finally سيتم استدعاؤه ما إذا كانت السلسلة الفرعية أدناه تنجح أو تفشل. في الرابط finally ، ننتقل إلى الخيط الرئيسي (السطر 7) واستدعاء singleUploadCompletion (السطر 8). في السطر 10 ، قمنا بتعيين حد أقصى للتوازي 1 (تنفيذ فردي) حول عملية التصدير (السطر 11). يتحول السطر 13 إلى قائمة الانتظار الخاصة المملوكة لمثيل managedObjectContext الخاص بنا. يعلن السطر 14 عن محاولة إعادة محاولة واحدة لعملية التحميل (السطر 15). يتحول السطر 17 إلى الخيط الرئيسي مرة أخرى ويستدعي الرقم 18 singleUploadSuccess . بحلول السطر الزمني سيتم تنفيذ 20 ، اكتملت جميع التكرارات المتوازية. إذا فشلت أقل من نصف التكرارات ، فحينئذٍ يتحول السطر 20 إلى قائمة الانتظار الرئيسية مرة أخيرة (تذكر أن كل منها تم تشغيله في قائمة انتظار الخلفية) ، ويسقط 21 القيمة الواردة (ما زالت mediaReferences ) ، ويستدعي 22 totalProcessSuccess .

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

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

الملحق: التأكد من الصحة التعاقدية للوظائف غير المتزامنة

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

لكن مساعدة المترجم هذه لا تنطبق عادة على الوظائف غير المتزامنة. ضع في اعتبارك المثال (المرح) التالي:

 func generateIcecream(from int: Int, completion: (String) -> Void) { if int > 5 { if int < 20 { completion("Chocolate") } else if int < 10 { completion("Strawberry") } completion("Pistachio") } else if int < 2 { completion("Vanilla") } }

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

اتضح أن هناك طريقة. لاحظ تعويذة Swifty التالية:

 func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int < 20 { completion("Chocolate") } else if int < 10 { completion("Strawberry") } // else completion("Pistachio") } else if int < 2 { completion("Vanilla") } }

الأسطر الأربعة المدرجة في الجزء العلوي من هذه الوظيفة تجبر المحول البرمجي على التحقق من استدعاء رد نداء الإكمال مرة واحدة بالضبط ، مما يعني أن هذه الوظيفة لم تعد مجمعة. ماذا يحدث هنا؟ في السطر الأول ، نعلن ولكن لا نهيئ النتيجة التي نريد في النهاية أن تنتجها هذه الوظيفة. من خلال تركها غير محددة ، نضمن أنه يجب تخصيصها مرة واحدة قبل أن يمكن استخدامها ، ومن خلال إعلانها نضمن عدم إمكانية تعيينها مرتين. السطر الثاني هو المؤجل الذي سيتم تنفيذه كإجراء نهائي لهذه الوظيفة. تستدعي كتلة الإكمال مع finalResult - بعد أن تم تعيينها بواسطة باقي الوظيفة. ينشئ السطر 3 ثابتًا جديدًا يسمى الإكمال والذي يعمل على تظليل معلمة معاودة الاتصال. الإكمال الجديد من النوع Void الذي لا يعلن عن API عام. يضمن هذا السطر أن أي استخدام للإكمال بعد هذا السطر سيكون خطأ في المترجم. التأجيل في السطر 2 هو الاستخدام الوحيد المسموح به لكتلة الإكمال. يزيل السطر 4 تحذير المترجم الذي قد يكون موجودًا بخلاف ذلك بشأن عدم استخدام ثابت الإكمال الجديد.

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

 func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int < 20 { finalResult = "Chocolate" } else if int < 10 { finalResult = "Strawberry" } // else finalResult = "Pistachio" } else if int < 2 { finalResult = "Vanilla" } }

الآن يقوم المترجم بالإبلاغ عن مشكلتين:

 error: AsyncCorrectness.playground:1:8: error: constant 'finalResult' used before being initialized defer { completion(finalResult) } ^ error: AsyncCorrectness.playground:11:3: error: immutable value 'finalResult' may only be initialized once finalResult = "Pistachio"

كما هو متوقع ، تحتوي الوظيفة على مسار حيث يتم تعيين finalResult صفر مرة وأيضًا مسار يتم تعيينه فيه أكثر من مرة. نقوم بحل هذه المشاكل على النحو التالي:

 func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int < 20 { finalResult = "Chocolate" } else if int < 10 { finalResult = "Strawberry" } else { finalResult = "Pistachio" } } else if int < 2 { finalResult = "Vanilla" } else { finalResult = "Neapolitan" } }

تم نقل "الفستق" إلى عبارة أخرى مناسبة وندرك أننا فشلنا في تغطية الحالة العامة - والتي هي بالطبع "نابولي".

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