Envelopper des séquences dans Swift

Bonjour à tous. Aujourd'hui, nous voulons partager la traduction préparée à la veille du lancement du cours «Développeur iOS. Cours avancé . " C'est parti!



L'un des principaux avantages de la conception basĂ©e sur le protocole de Swift est qu'il nous permet d'Ă©crire du code gĂ©nĂ©rique compatible avec un large Ă©ventail de types et non spĂ©cifiquement implĂ©mentĂ© pour tout le monde. Surtout si un tel code gĂ©nĂ©ral est destinĂ© Ă  l'un des protocoles, qui peut ĂȘtre trouvĂ© dans la bibliothĂšque standard, ce qui permettra de l'utiliser Ă  la fois avec les types intĂ©grĂ©s et ceux dĂ©finis par l'utilisateur.


Un exemple d'un tel protocole est Sequence, qui est acceptĂ© par tous les types de bibliothĂšques standard qui peuvent ĂȘtre itĂ©rĂ©es, comme Array, Dictionary, Set et bien d'autres. Cette semaine, voyons comment nous pouvons encapsuler Sequence dans des conteneurs universels, ce qui nous permettra d'encapsuler divers algorithmes au cƓur des API faciles Ă  utiliser.


L'art d'ĂȘtre paresseux


Il est assez facile de se perdre en pensant que toutes les sĂ©quences sont similaires Ă  Array, car tous les Ă©lĂ©ments sont instantanĂ©ment chargĂ©s en mĂ©moire lorsque la sĂ©quence est créée. Étant donnĂ© que la seule exigence du protocole de sĂ©quence est que les rĂ©cepteurs doivent ĂȘtre capables d'itĂ©rer, nous ne pouvons faire aucune hypothĂšse sur la façon dont les Ă©lĂ©ments d'une sĂ©quence inconnue sont chargĂ©s ou stockĂ©s.
Par exemple, comme nous l'avons vu dans SĂ©quences rapides: l'art d'ĂȘtre paresseux , les sĂ©quences peuvent parfois charger leurs Ă©lĂ©ments paresseusement - soit pour des raisons de performances, soit parce qu'il n'est pas garanti que la sĂ©quence entiĂšre puisse tenir en mĂ©moire. Voici quelques exemples de telles sĂ©quences:


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

Étant donnĂ© que toutes les sĂ©quences ci-dessus sont paresseuses pour une raison quelconque, nous ne voudrions pas les forcer dans un tableau, par exemple, en appelant Array (folder.subfolders). Mais nous pouvons toujours vouloir les modifier et les utiliser de diffĂ©rentes maniĂšres, alors regardons comment nous pouvons le faire en crĂ©ant un type d'encapsuleur de sĂ©quence.


Création de fondation


Commençons par créer un type de base que nous pouvons utiliser pour créer toutes sortes d'API pratiques au-dessus de n'importe quelle séquence. Nous l'appellerons WrappedSequence, et ce sera un type universel contenant à la fois le type de la séquence que nous encapsulons et le type d'élément que nous voulons que notre nouvelle séquence crée.
La principale caractéristique de notre wrapper sera sa fonction IteratorFunction, qui nous permettra de prendre le contrÎle de la recherche de la séquence de base - en changeant l'itérateur utilisé pour chaque itération:


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

Comme vous pouvez le voir ci-dessus, Sequence utilise un modÚle d'usine de sorte que chaque séquence crée une nouvelle instance d'itérateur pour chaque itération - en utilisant la méthode makeIterator ().


Ci-dessus, nous utilisons le type AnyIterator de la bibliothÚque standard, qui est un itérateur d'effacement de type qui peut utiliser n'importe quelle implémentation IteratorProtocol de base pour obtenir les valeurs des éléments. Dans notre cas, nous allons créer un élément en appelant notre IteratorFunction, en passant comme argument notre propre itérateur de la séquence encapsulée, et puisque cet argument est marqué comme inout, nous pouvons changer l'itérateur de base en place à l'intérieur de notre fonction.


Étant donnĂ© que WrappedSequence est Ă©galement une sĂ©quence, nous pouvons utiliser toutes les fonctionnalitĂ©s de la bibliothĂšque standard qui lui sont associĂ©es, telles que l'itĂ©rer dessus ou transformer ses valeurs Ă  l'aide de la carte:


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

Commençons maintenant avec notre nouvelle WrappedSequence!


Préfixes et suffixes


Lorsque vous travaillez trÚs souvent avec des séquences, vous souhaitez insérer un préfixe ou un suffixe dans la séquence avec laquelle nous travaillons - mais ne serait-il pas formidable de pouvoir le faire sans changer la séquence principale? Cela peut conduire à de meilleures performances et nous permet d'ajouter des préfixes et suffixes à n'importe quelle séquence, et pas seulement des types généraux tels que Array.


En utilisant WrappedSequence, nous pouvons le faire assez facilement. Tout ce que nous devons faire est d'étendre Sequence avec une méthode qui crée une séquence encapsulée à partir d'un tableau d'éléments à insérer comme préfixe. Ensuite, lorsque nous répétons, nous commençons à itérer sur tous les éléments de préfixe avant de continuer avec la séquence de base - comme ceci:


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

Ci-dessus, nous utilisons un paramĂštre avec un nombre variable d'arguments (en ajoutant ... Ă  son type) pour permettre la transmission d'un ou plusieurs Ă©lĂ©ments Ă  la mĂȘme mĂ©thode.
De la mĂȘme maniĂšre, nous pouvons crĂ©er une mĂ©thode qui ajoute un ensemble donnĂ© de suffixes Ă  la fin de la sĂ©quence - d'abord en effectuant notre propre itĂ©ration de la sĂ©quence de base, puis en itĂ©rant sur les Ă©lĂ©ments suffixĂ©s:


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

Avec les deux méthodes mentionnées ci-dessus, nous pouvons maintenant ajouter des préfixes et des suffixes à n'importe quelle séquence que nous voulons. Voici quelques exemples d'utilisation de nos nouvelles API:


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

Bien que tous les exemples ci-dessus puissent ĂȘtre implĂ©mentĂ©s Ă  l'aide de types spĂ©cifiques (tels que Array et String), l'avantage d'utiliser notre type WrappedSequence est que tout peut ĂȘtre fait paresseusement - nous n'effectuons aucune mutation ni n'Ă©valuons aucune sĂ©quence pour ajouter notre prĂ©fixes ou suffixes - qui peuvent ĂȘtre trĂšs utiles dans des situations critiques pour les performances ou lorsque vous travaillez avec de grands ensembles de donnĂ©es.


Segmentation


Ensuite, voyons comment nous pouvons encapsuler des séquences pour en créer des versions segmentées. Dans certaines itérations, il ne suffit pas de savoir ce qu'est l'élément actuel - nous pouvons également avoir besoin d'informations sur les éléments suivants et précédents.
Lorsque vous travaillez avec des séquences indexées, nous pouvons souvent y parvenir en utilisant l'API énumérée (), qui utilise également un wrapper de séquence pour nous donner accÚs à la fois à l'élément actuel et à son index:


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

Cependant, la technique ci-dessus n'est pas seulement assez verbeuse en termes d'invocation, elle repose également sur l'utilisation de tableaux à nouveau - ou au moins d'une forme de séquence qui nous donne un accÚs aléatoire à ses éléments - que de nombreuses séquences, en particulier les paresseuses pas la bienvenue.
À la place, utilisons Ă  nouveau notre WrappedSequence - pour crĂ©er un encapsuleur de sĂ©quence qui fournit paresseusement des vues segmentĂ©es dans sa sĂ©quence de base, en suivant les Ă©lĂ©ments prĂ©cĂ©dents et actuels et en les mettant Ă  jour au fur et Ă  mesure de son itĂ©ration:


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

Maintenant, nous pouvons utiliser l'API ci-dessus pour créer une version segmentée de n'importe quelle séquence chaque fois que nous devons regarder en avant ou en arriÚre lors d'une itération. Par exemple, voici comment nous pouvons utiliser la segmentation afin que nous puissions facilement déterminer quand nous avons atteint la fin de la liste:


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

Nous commençons maintenant à voir la véritable puissance des séquences d'encapsulation - en ce sens qu'elles nous permettent de masquer des algorithmes de plus en plus complexes dans une API vraiment simple. Tout ce dont l'appelant a besoin pour segmenter la séquence, c'est accéder à la propriété segmentée dans n'importe quelle séquence, et notre implémentation de base se chargera du reste.


Récursivité


Enfin, regardons comment mĂȘme les itĂ©rations rĂ©cursives peuvent ĂȘtre modĂ©lisĂ©es Ă  l'aide d'encapsuleurs de sĂ©quence. Supposons que nous voulions fournir un moyen simple d'itĂ©rer rĂ©cursivement une hiĂ©rarchie de valeurs dans laquelle chaque Ă©lĂ©ment de la hiĂ©rarchie contient une sĂ©quence d'Ă©lĂ©ments enfants. Il peut ĂȘtre assez difficile de le faire correctement, donc ce serait gĂ©nial si nous pouvions utiliser une implĂ©mentation pour effectuer toutes ces itĂ©rations dans notre base de code.
En utilisant WrappedSequence, nous pouvons y parvenir en Ă©tendant Sequence avec une mĂ©thode qui utilise la mĂȘme contrainte de type gĂ©nĂ©rique pour garantir que chaque Ă©lĂ©ment peut fournir une sĂ©quence imbriquĂ©e qui a le mĂȘme type d'itĂ©rateur que notre original. Pour pouvoir accĂ©der dynamiquement Ă  chaque sĂ©quence imbriquĂ©e, nous demanderons Ă©galement Ă  l'appelant de spĂ©cifier KeyPath pour la propriĂ©tĂ© qui doit ĂȘtre utilisĂ©e pour la rĂ©cursivitĂ©, ce qui nous donnera une implĂ©mentation qui ressemble Ă  ceci:


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

En utilisant ce qui précÚde, nous pouvons maintenant itérer récursivement sur n'importe quelle séquence, quelle que soit la façon dont elle est construite à l'intérieur, et sans avoir à charger la hiérarchie entiÚre à l'avance. Par exemple, voici comment nous pourrions utiliser cette nouvelle API pour parcourir récursivement une hiérarchie de dossiers:


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

Nous pouvons Ă©galement l'utiliser pour itĂ©rer sur tous les nƓuds de l'arborescence ou pour parcourir rĂ©cursivement un ensemble d'enregistrements de base de donnĂ©es - par exemple, pour rĂ©pertorier tous les groupes d'utilisateurs dans une organisation:


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

Une chose que nous devons ĂȘtre prudents lorsqu'il s'agit d'itĂ©rations rĂ©cursives est d'empĂȘcher les rĂ©fĂ©rences circulaires - lorsqu'un certain chemin nous ramĂšne Ă  un Ă©lĂ©ment que nous avons dĂ©jĂ  rencontrĂ© - qui nous mĂšnera Ă  une boucle infinie.
Une façon de rĂ©soudre ce problĂšme est de garder une trace de tous les Ă©lĂ©ments qui se produisent (mais cela peut ĂȘtre problĂ©matique d'un point de vue mĂ©moire), de s'assurer qu'il n'y a pas de rĂ©fĂ©rences circulaires dans notre ensemble de donnĂ©es, ou de gĂ©rer de tels cas Ă  chaque fois du cĂŽtĂ© de l'appel (en utilisant break, continue ou return pour terminer tout itĂ©rations cycliques).


Conclusion


La séquence est l'un des protocoles les plus simples de la bibliothÚque standard - elle ne nécessite qu'une seule méthode - mais elle est toujours l'une des plus puissantes, en particulier en ce qui concerne la quantité de fonctionnalités que nous pouvons créer en fonction de celle-ci. Tout comme la bibliothÚque standard contient des séquences d'encapsuleur pour des choses comme les énumérations, nous pouvons également créer nos propres encapsuleurs - qui nous permettent de masquer des fonctionnalités avancées avec des API vraiment simples.


Bien que les abstractions aient toujours un prix, il est important de considĂ©rer quand cela vaut la peine (et peut-ĂȘtre plus important encore quand elles n'en valent pas la peine) de les introduire, si nous pouvons construire nos abstractions directement en plus de ce que la bibliothĂšque standard fournit - en utilisant les mĂȘmes conventions - alors ces les abstractions sont gĂ©nĂ©ralement plus susceptibles de rĂ©sister Ă  l'Ă©preuve du temps.

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


All Articles