Le modèle architectural de l'itérateur dans l'univers Swift

«Iterator» est l'un des modèles de conception que les programmeurs ne remarquent pas le plus souvent, car sa mise en œuvre, en règle générale, est intégrée directement dans les outils standard du langage de programmation. Cependant, c'est aussi l'un des modèles de comportement décrits dans le livre «Gang of Four», «GoF», «Design Patterns: Elements of Reusable Object-Oriented Software» , et comprendre son appareil ne fait jamais de mal, et parfois il peut même aider dans quelque chose.

Un «itérateur» est une méthode d'accès séquentiel à tous les éléments d'un objet composite (en particulier, les types de conteneurs, tels qu'un tableau ou un ensemble).

Outils de langage standard


Créez une sorte de tableau :

let numbersArray = [0, 1, 2] 

... puis "marchez" Ă  travers lui en un cycle :

 for number in numbersArray { print(number) } 

... semble être une action très naturelle, en particulier pour les langages de programmation modernes comme Swift . Cependant, dans les coulisses de cette action simple se trouve un code qui implémente les principes du modèle Iterator.

Dans «Swift», afin de pouvoir «itérer» une variable en utilisant for -cycles, le type de variable doit implémenter le protocole Sequence . Entre autres choses, ce protocole nécessite que le type soit associatedtype un Iterator , qui à son tour doit implémenter les exigences du protocole IteratorProtocol , ainsi que la méthode d'usine makeIterator() , qui renvoie un "itérateur" spécifique pour ce type:

 protocol Sequence { associatedtype Iterator : IteratorProtocol func makeIterator() -> Self.Iterator // Another requirements go here… } 

Le protocole IteratorProtocol , à son tour, ne contient qu'une seule méthode - next() , qui renvoie l'élément suivant dans la séquence:

 protocol IteratorProtocol { associatedtype Element mutating func next() -> Self.Element? } 

Cela ressemble à «beaucoup de code compliqué», mais ce n'est pas le cas. Ci-dessous, nous verrons cela.

Le type Array implémente le protocole Sequence (mais pas directement, mais via la chaîne d' héritage du protocole : le MutableCollection hérite des exigences Collection , et ce dernier hérite des exigences Sequence ), de sorte que ses instances, en particulier, peuvent être «itérées» à l'aide de for -cycles.

Types personnalisés


Que faut-il faire pour pouvoir itérer votre propre type? Comme cela arrive souvent, il est plus facile de montrer un exemple.

Supposons qu'il existe un type représentant une étagère qui stocke un certain ensemble d'instances d'une classe, qui à son tour représente un livre:

 struct Book { let author: String let title: String } struct Shelf { var books: [Book] } 

Pour pouvoir "itérer" une instance de la classe Shelf , cette classe doit répondre aux exigences du protocole Sequence . Pour cet exemple, il suffira d'implémenter la méthode makeIterator() , d'autant plus que les autres exigences du protocole ont des implémentations par défaut . Cette méthode doit renvoyer une instance d'un type qui implémente le protocole IteratorProtocol . Heureusement, dans le cas de Swift, c'est très peu de code très simple:

 struct ShelfIterator: IteratorProtocol { private var books: [Book] init(books: [Book]) { self.books = books } mutating func next() -> Book? { // TODO: Return next underlying Book element. } } extension Shelf: Sequence { func makeIterator() -> ShelfIterator { return ShelfIterator(books: books) } } 

La méthode next() du type ShelfIterator déclarée en mutating , car l'instance de type doit en quelque sorte stocker l'état correspondant à l'itération actuelle:

 mutating func next() -> Book? { defer { if !books.isEmpty { books.removeFirst() } } return books.first } 

Cette option d'implémentation renvoie toujours le premier élément de la séquence, ou nil si la séquence est vide. Le bloc defer «encapsulé» avec le code pour changer la collection itérée, ce qui supprime l'élément de la dernière étape d'itération immédiatement après le retour de la méthode.

Exemple d'utilisation:

 let book1 = Book(author: ". ", title: "") let book2 = Book(author: ". ", title: " ") let book3 = Book(author: ". ", title: " ") let shelf = Shelf(books: [book1, book2, book3]) for book in shelf { print("\(book.author) – \(book.title)") } /* .  –  .  –   .  –   */ 

Parce que Étant donné que tous les types utilisés (y compris le Array sous-jacent du Array ) sont basés sur la sémantique des valeurs (par opposition aux références) , vous n'avez pas à vous soucier de la valeur de la variable d'origine modifiée pendant l'itération. Lors de la gestion de types basés sur la sémantique des liens, ce point doit être gardé à l'esprit et pris en compte lors de la création de vos propres itérateurs.

Fonctionnalité classique


L '«itérateur» classique décrit dans le livre «Gangs of Four», en plus de renvoyer l'élément suivant de la séquence itérable, peut également à tout moment renvoyer l'élément actuel dans le processus d'itération, le premier élément de la séquence itérable et la valeur du «drapeau» indiquant s'il y a encore éléments dans une séquence itérée par rapport à l'étape d'itération en cours.

Bien sûr, il serait facile de déclarer un protocole élargissant ainsi les capacités du protocole IteratorProtocol standard:

 protocol ClassicIteratorProtocol: IteratorProtocol { var currentItem: Element? { get } var first: Element? { get } var isDone: Bool { get } } 

Le premier et les éléments actuels sont retournés facultatifs puisque la séquence source peut être vide.

Option d'implémentation simple:

 struct ShelfIterator: ClassicIteratorProtocol { var currentItem: Book? = nil var first: Book? var isDone: Bool = false private var books: [Book] init(books: [Book]) { self.books = books first = books.first currentItem = books.first } mutating func next() -> Book? { currentItem = books.first books.removeFirst() isDone = books.isEmpty return books.first } } 

Dans la description d'origine du modèle, la méthode next() modifie l'état interne de l'itérateur pour passer à l'élément suivant et est de type Void , et l'élément actuel est renvoyé par la méthode currentElement() . Dans le protocole IteratorProtocol , ces deux fonctions sont comme si elles étaient combinées en une seule.

La nécessité de la first() méthode first() est également douteuse, car l'itérateur ne change pas la séquence d'origine, et nous avons toujours la possibilité d'accéder à son premier élément (le cas échéant, bien sûr).

Et, puisque la méthode next() renvoie nil lorsque l'itération est terminée, la méthode isDone() devient également inutile.

Cependant, à des fins académiques, il est tout à fait possible de proposer une fonction qui pourrait utiliser toutes les fonctionnalités:

 func printShelf(with iterator: inout ShelfIterator) { var bookIndex = 0 while !iterator.isDone { bookIndex += 1 print("\(bookIndex). \(iterator.currentItem!.author) – \(iterator.currentItem!.title)") _ = iterator.next() } } var iterator = ShelfIterator(books: shelf.books) printShelf(with: &iterator) /* 1. .  –  2. .  –   3. .  –   */ 

Le paramètre iterator est déclaré inout car son état interne change lors de l'exécution de la fonction. Et lorsque la fonction est appelée, l'instance d'itérateur est transmise non pas directement par sa propre valeur, mais par référence.

Le résultat de l'appel de la méthode next() n'est pas utilisé, simulant l'absence de la valeur de retour de l'implémentation du manuel.

Au lieu d'une conclusion


Cela semble être tout ce que je voulais dire cette fois. Tout beau code et écrit délibérément!

Mes autres articles sur les modèles de conception:

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


All Articles