Wrap-Sequenzen in Swift

Hallo an alle. Heute möchten wir die Übersetzung teilen, die am Vorabend des Starts des Kurses „iOS Developer. Fortgeschrittenenkurs . Lass uns gehen!



Einer der Hauptvorteile des protokollbasierten Designs von Swift besteht darin, dass wir generischen Code schreiben können, der mit einer Vielzahl von Typen kompatibel ist, anstatt speziell für alle implementiert zu werden. Insbesondere, wenn ein solcher allgemeiner Code für eines der Protokolle vorgesehen ist, die in der Standardbibliothek enthalten sind, sodass er sowohl mit integrierten als auch mit benutzerdefinierten Typen verwendet werden kann.


Ein Beispiel für ein solches Protokoll ist Sequence, das von allen Arten von Standardbibliotheken akzeptiert wird, die iteriert werden können, z. B. Array, Dictionary, Set und viele andere. Schauen wir uns diese Woche an, wie wir Sequence in universelle Container verpacken können, um verschiedene Algorithmen im Kern benutzerfreundlicher APIs zu kapseln.


Die Kunst, faul zu sein


Es ist ziemlich leicht zu verwirren, wenn man denkt, dass alle Sequenzen dem Array ähnlich sind, da alle Elemente beim Erstellen der Sequenz sofort in den Speicher geladen werden. Da die einzige Anforderung des Sequenzprotokolls darin besteht, dass Empfänger iterieren können müssen, können wir keine Annahmen darüber treffen, wie Elemente einer unbekannten Sequenz geladen oder gespeichert werden.
Wie wir beispielsweise in Swift Sequences: The Art of Being Lazy beschrieben haben , können Sequenzen ihre Elemente manchmal träge laden - entweder aus Leistungsgründen oder weil nicht garantiert ist, dass die gesamte Sequenz in den Speicher passt. Hier sind einige Beispiele für solche Sequenzen:


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

Da alle oben genannten Sequenzen aus irgendeinem Grund faul sind, möchten wir sie nicht in ein Array zwingen, indem wir beispielsweise Array (folder.subfolders) aufrufen. Möglicherweise möchten wir sie jedoch noch auf unterschiedliche Weise ändern und damit arbeiten. Schauen wir uns also an, wie wir dies tun können, indem wir eine Art Sequenz-Wrapper erstellen.


Gründung der Stiftung


Beginnen wir mit der Erstellung eines Basistyps, mit dem wir alle Arten von praktischen APIs über einer beliebigen Sequenz erstellen können. Wir werden es WrappedSequence nennen, und es wird ein universeller Typ sein, der sowohl den Typ der Sequenz enthält, die wir umschließen, als auch den Typ des Elements, das unsere neue Sequenz erstellen soll.
Das Hauptmerkmal unseres Wrappers wird seine IteratorFunction sein, mit der wir die Kontrolle über die Suche nach der Basissequenz übernehmen und den für jede Iteration verwendeten Iterator ändern können:


 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) } } } 

Wie Sie oben sehen können, verwendet Sequence ein Factory-Muster, sodass für jede Sequenz eine neue Iteratorinstanz für jede Iteration erstellt wird - mithilfe der Methode makeIterator ().


Oben verwenden wir den AnyIterator-Typ der Standardbibliothek, einen Iterator vom Typ Löschen , der jede grundlegende IteratorProtocol-Implementierung verwenden kann, um Elementwerte abzurufen. In unserem Fall erstellen wir ein Element, indem wir unsere IteratorFunction aufrufen und als Argument unseren eigenen Iterator der umschlossenen Sequenz übergeben. Da dieses Argument als inout markiert ist, können wir den Basisiterator innerhalb unserer Funktion ändern.


Da WrappedSequence auch eine Sequenz ist, können wir alle damit verbundenen Funktionen der Standardbibliothek nutzen, z. B. iterieren oder ihre Werte mithilfe von map transformieren:


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

Beginnen wir jetzt mit unserer neuen WrappedSequence!


Präfixe und Suffixe


Wenn wir sehr oft mit Sequenzen arbeiten, besteht der Wunsch, ein Präfix oder Suffix in die Sequenz einzufügen, mit der wir arbeiten - aber wäre es nicht großartig, wenn wir dies tun könnten, ohne die Hauptsequenz zu ändern? Dies kann zu einer besseren Leistung führen und ermöglicht es uns, jeder Sequenz Präfixe und Suffixe hinzuzufügen, und nicht nur allgemeinen Typen wie Array.


Mit WrappedSequence können wir dies ganz einfach tun. Alles, was wir tun müssen, ist, Sequence mit einer Methode zu erweitern, die eine umschlossene Sequenz aus einem Array von Elementen erstellt, die als Präfix eingefügt werden sollen. Wenn wir dann iterieren, beginnen wir, alle Präfixelemente zu iterieren, bevor wir mit der Basissequenz fortfahren - wie folgt:


 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() } } } 

Oben verwenden wir einen Parameter mit einer variablen Anzahl von Argumenten (Hinzufügen ... zu seinem Typ), um die Übertragung eines oder mehrerer Elemente an dieselbe Methode zu ermöglichen.
Auf die gleiche Weise können wir eine Methode erstellen, die am Ende der Sequenz einen bestimmten Satz von Suffixen hinzufügt - indem wir zuerst unsere eigene Iteration der Basissequenz durchführen und dann über die Suffixelemente iterieren:


 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 } } } 

Mit den beiden oben genannten Methoden können wir jetzt jeder gewünschten Sequenz Präfixe und Suffixe hinzufügen. Hier einige Beispiele, wie unsere neuen APIs verwendet werden können:


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

Obwohl alle oben genannten Beispiele mit bestimmten Typen (wie Array und String) implementiert werden können, besteht der Vorteil der Verwendung unseres WrappedSequence-Typs darin, dass alles träge ausgeführt werden kann - wir führen keine Mutationen durch oder bewerten keine Sequenzen, um unsere hinzuzufügen Präfixe oder Suffixe - die in leistungskritischen Situationen oder bei der Arbeit mit großen Datenmengen sehr nützlich sein können.


Segmentierung


Als nächstes schauen wir uns an, wie wir Sequenzen umbrechen können, um segmentierte Versionen davon zu erstellen. In bestimmten Iterationen reicht es nicht aus, das aktuelle Element zu kennen. Möglicherweise benötigen wir auch Informationen zu den nächsten und vorherigen Elementen.
Wenn wir mit indizierten Sequenzen arbeiten, können wir dies häufig mithilfe der enumerated () -API erreichen, die auch einen Sequenz-Wrapper verwendet, um Zugriff auf das aktuelle Element und seinen Index zu erhalten:


 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 ... } 

Die obige Technik ist jedoch nicht nur in Bezug auf den Aufruf ziemlich ausführlich, sondern beruht auch auf der erneuten Verwendung von Arrays - oder zumindest einer Form von Sequenz, die uns zufälligen Zugriff auf ihre Elemente ermöglicht -, dass viele Sequenzen, insbesondere faule, nicht willkommen.
Verwenden wir stattdessen erneut unsere WrappedSequence, um einen Sequenz-Wrapper zu erstellen, der träge segmentierte Ansichten in seiner Basissequenz bereitstellt, frühere und aktuelle Elemente verfolgt und diese aktualisiert, während sie weiter durchlaufen:


 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 } } } 

Jetzt können wir die obige API verwenden, um eine segmentierte Version einer beliebigen Sequenz zu erstellen, wenn wir bei einer Iteration entweder vorwärts oder rückwärts schauen müssen. Hier ist zum Beispiel, wie wir die Segmentierung verwenden können, damit wir leicht feststellen können, wann wir das Ende der Liste erreicht haben:


 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) } 

Jetzt beginnen wir, die wahre Kraft des Wickelns von Sequenzen zu erkennen - indem sie es uns ermöglichen, immer komplexere Algorithmen in einer wirklich einfachen API zu verbergen. Alles, was der Aufrufer zum Segmentieren der Sequenz benötigt, ist der Zugriff auf die segmentierte Eigenschaft in einer beliebigen Sequenz, und unsere grundlegende Implementierung kümmert sich um den Rest.


Rekursion


Schauen wir uns abschließend an, wie selbst rekursive Iterationen mithilfe von Sequenz-Wrappern modelliert werden können. Angenommen, wir möchten eine einfache Möglichkeit bieten, eine Wertehierarchie rekursiv zu durchlaufen, in der jedes Element in der Hierarchie eine Folge von untergeordneten Elementen enthält. Es kann ziemlich schwierig sein, es richtig zu machen, daher wäre es großartig, wenn wir eine Implementierung verwenden könnten, um alle derartigen Iterationen in unserer Codebasis durchzuführen.
Mit WrappedSequence können wir dies erreichen, indem wir Sequence um eine Methode erweitern, die dieselbe generische Typbeschränkung verwendet, um sicherzustellen, dass jedes Element eine verschachtelte Sequenz mit demselben Iteratortyp wie unser Original bereitstellen kann. Um dynamisch auf jede verschachtelte Sequenz zugreifen zu können, werden wir den Aufrufer außerdem bitten, KeyPath für die Eigenschaft anzugeben, die für die Rekursion verwendet werden soll. Dadurch erhalten wir eine Implementierung, die folgendermaßen aussieht:


 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 } } } 

Mit dem oben Gesagten können wir jetzt jede Sequenz rekursiv durchlaufen, unabhängig davon, wie sie im Inneren aufgebaut ist, und ohne die gesamte Hierarchie im Voraus laden zu müssen. Hier ist beispielsweise, wie wir diese neue API verwenden können, um eine Ordnerhierarchie rekursiv zu durchlaufen:


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

Wir können es auch verwenden, um über alle Knoten des Baums zu iterieren oder eine Reihe von Datenbankdatensätzen rekursiv zu durchlaufen - um beispielsweise alle Benutzergruppen in einer Organisation aufzulisten:


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

Eine Sache, bei der wir bei rekursiven Iterationen vorsichtig sein müssen, ist die Vermeidung von Zirkelverweisen - wenn ein bestimmter Pfad uns zu einem Element zurückführt, auf das wir bereits gestoßen sind -, was uns zu einer Endlosschleife führt.
Eine Möglichkeit, dies zu beheben, besteht darin, alle auftretenden Elemente im Auge zu behalten (dies kann jedoch aus Speichersicht problematisch sein), sicherzustellen, dass unser Datensatz keine Zirkelverweise enthält, oder solche Fälle jedes Mal von der Anrufseite aus zu behandeln (mithilfe von break, continue oder return, um alle zu vervollständigen) zyklische Iterationen).


Fazit


Sequence ist eines der einfachsten Protokolle in der Standardbibliothek - es erfordert nur eine Methode -, aber es ist immer noch eines der leistungsstärksten, insbesondere wenn es darum geht, wie viele Funktionen wir basierend darauf erstellen können. So wie die Standardbibliothek Wrapper-Sequenzen für Aufzählungen enthält, können wir auch eigene Wrapper erstellen, mit denen wir erweiterte Funktionen mit wirklich einfachen APIs verbergen können.


Obwohl Abstraktionen immer ihren Preis haben, ist es wichtig zu überlegen, wann es sich lohnt (und vielleicht noch wichtiger, wenn es sich nicht lohnt), sie einzuführen, wenn wir unsere Abstraktionen direkt auf dem aufbauen können, was die Standardbibliothek bietet - unter Verwendung derselben Konventionen -, dann diese Abstraktionen halten sich normalerweise eher im Laufe der Zeit.

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


All Articles