«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
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? {
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)
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: