التفاف تسلسل في سويفت

مرحبا بالجميع. اليوم نريد أن نشارك الترجمة التي تم إعدادها عشية إطلاق الدورة التدريبية "iOS Developer. دورة متقدمة . " دعنا نذهب!



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


مثال على هذا البروتوكول هو التسلسل ، وهو مقبول من قبل جميع أنواع المكتبات القياسية التي يمكن تكرارها ، مثل Array ، و Dictionary ، و Set ، وغيرها الكثير. دعونا نلقي نظرة هذا الأسبوع على الطريقة التي يمكننا بها لف التسلسل في حاويات عالمية ، مما سيسمح لنا بتغليف العديد من الخوارزميات في قلب واجهات برمجة التطبيقات سهلة الاستخدام.


فن كونك كسول


من السهل أن تشعر بالارتباك من خلال التفكير في أن جميع التسلسلات تشبه Array ، حيث يتم تحميل جميع العناصر على الفور في الذاكرة عند إنشاء التسلسل. نظرًا لأن الشرط الوحيد لبروتوكول التسلسل هو أن المستقبلات يجب أن تكون قادرة على التكرار ، لا يمكننا أن نضع أي افتراضات حول كيفية تحميل عناصر من تسلسل غير معروف أو تخزينها.
على سبيل المثال ، كما غطينا في Swift Sequences: The Art of Being Lazy ، يمكن للتسلسل في بعض الأحيان تحميل عناصرها بتكاسل - إما لأسباب تتعلق بالأداء أو لأنه غير مضمون بأن التسلسل بأكمله يمكن أن يتلاءم مع الذاكرة. فيما يلي بعض الأمثلة على هذه التسلسلات:


//   ,          ,           . let records = database.records(matching: searchQuery) //     ,       ,      . let folders = folder.subfolders //   ,     ,            . let nodes = node.children 

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


إنشاء الأساس


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


 struct WrappedSequence<Wrapped: Sequence, Element>: Sequence { typealias IteratorFunction = (inout Wrapped.Iterator) -> Element? private let wrapped: Wrapped private let iterator: IteratorFunction init(wrapping wrapped: Wrapped, iterator: @escaping IteratorFunction) { self.wrapped = wrapped self.iterator = iterator } func makeIterator() -> AnyIterator<Element> { var wrappedIterator = wrapped.makeIterator() return AnyIterator { self.iterator(&wrappedIterator) } } } 

كما ترون أعلاه ، يستخدم Sequence نمط مصنع بحيث ينشئ كل تسلسل نسخة متكررة جديدة لكل تكرار - باستخدام طريقة makeIterator ().


في الأعلى ، نستخدم نوع AnyIterator في المكتبة القياسية ، وهو مكرر لمحو الكتابة الذي يمكنه استخدام أي تطبيق IteratorProtocol أساسي للحصول على قيم Element. في حالتنا ، سننشئ عنصرًا عن طريق استدعاء IteratorFunction لدينا ، بتمرير وسيطة خاصة بنا للتسلسل المُلتف ، وبما أن هذه الوسيطة مُعلَّمة بالداخل ، فيمكننا تغيير أداة التكرار الأساسية في مكانها داخل وظيفتنا.


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


 let folderNames = WrappedSequence(wrapping: folders) { iterator in return iterator.next()?.name } for name in folderNames { ... } let uppercasedNames = folderNames.map { $0.uppercased() } 

الآن دعونا نبدأ مع WrappedSequence الجديد!


البادئات واللواحق


عند العمل بالتسلسلات في كثير من الأحيان ، هناك رغبة في إدراج بادئة أو لاحقة في التسلسل الذي نعمل به - لكن هل سيكون من الرائع أن نتمكن من القيام بذلك دون تغيير التسلسل الرئيسي؟ يمكن أن يؤدي هذا إلى أداء أفضل ويسمح لنا بإضافة بادئات ولاحقات إلى أي تسلسل ، وليس فقط الأنواع العامة مثل Array.


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


 extension Sequence { func prefixed( with prefixElements: Element... ) -> WrappedSequence<Self, Element> { var prefixIndex = 0 return WrappedSequence(wrapping: self) { iterator in //        ,   ,   ,   : guard prefixIndex >= prefixElements.count else { let element = prefixElements[prefixIndex] prefixIndex += 1 return element } //           : return iterator.next() } } } 

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


 extension Sequence { func suffixed( with suffixElements: Element... ) -> WrappedSequence<Self, Element> { var suffixIndex = 0 return WrappedSequence(wrapping: self) { iterator in guard let next = iterator.next() else { //    ,     nil      : guard suffixIndex < suffixElements.count else { return nil } let element = suffixElements[suffixIndex] suffixIndex += 1 return element } return next } } } 

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


 //      : let allFolders = rootFolder.subfolders.prefixed(with: rootFolder) //       : let messages = inbox.messages.suffixed(with: composer.message) //       ,      : let characters = code.prefixed(with: "{").suffixed(with: "}") 

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


Cegmentatsiya


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


 for (index, current) in list.items.enumerated() { let previous = (index > 0) ? list.items[index - 1] : nil let next = (index < list.items.count - 1) ? list.items[index + 1] : nil ... } 

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


 extension Sequence { typealias Segment = ( previous: Element?, current: Element, next: Element? ) var segmented: WrappedSequence<Self, Segment> { var previous: Element? var current: Element? var endReached = false return WrappedSequence(wrapping: self) { iterator in //        ,      ,   ,        ,     . guard !endReached, let element = current ?? iterator.next() else { return nil } let next = iterator.next() let segment = (previous, element, next) //     ,    ,      : previous = element current = next endReached = (next == nil) return segment } } } 

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


 for segment in list.items.segmented { addTopBorder() addView(for: segment.current) if segment.next == nil { //   ,     addBottomBorder() } } ```swift        ,   .    ,               : ```swift for segment in path.nodes.segmented { let directions = ( enter: segment.previous?.direction(to: segment.current), exit: segment.next.map(segment.current.direction) ) let nodeView = NodeView(directions: directions) nodeView.center = segment.current.position.cgPoint view.addSubview(nodeView) } 

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


العودية


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


 extension Sequence { func recursive<S: Sequence>( for keyPath: KeyPath<Element, S> ) -> WrappedSequence<Self, Element> where S.Iterator == Iterator { var parentIterators = [Iterator]() func moveUp() -> (iterator: Iterator, element: Element)? { guard !parentIterators.isEmpty else { return nil } var iterator = parentIterators.removeLast() guard let element = iterator.next() else { //          ,    ,      : return moveUp() } return (iterator, element) } return WrappedSequence(wrapping: self) { iterator in //       ,      ,      : let element = iterator.next() ?? { return moveUp().map { iterator = $0 return $1 } }() //       ,  ,         ,         . if let nested = element?[keyPath: keyPath].makeIterator() { let parent = iterator parentIterators.append(parent) iterator = nested } return element } } } 

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


 let allFolders = folder.subfolders.recursive(for: \.subfolders) for folder in allFolders { try loadContent(from: folder) } 

يمكننا أيضًا استخدامه للتكرار عبر جميع عقد الشجرة أو لاجتياز مجموعة من سجلات قاعدة البيانات بشكل متكرر - على سبيل المثال ، لسرد جميع مجموعات المستخدمين في مؤسسة:


 let allNodes = tree.recursive(for: \.children) let allGroups = database.groups.recusive(for: \.subgroups) 

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


استنتاج


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


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

Source: https://habr.com/ru/post/ar464995/


All Articles