„Iterator“ ist eines der Entwurfsmuster, die Programmierer am häufigsten nicht bemerken, da seine Implementierung in der Regel direkt in die Standardwerkzeuge der Programmiersprache integriert ist. Dies ist jedoch auch eines der Verhaltensmuster, die im
Buch „Gang of Four“, „GoF“, „Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software“ beschrieben und verstanden werden Sein Gerät tut nie weh und manchmal kann es sogar bei etwas helfen.
Ein „Iterator“ ist eine Methode für den sequentiellen Zugriff auf alle Elemente eines zusammengesetzten Objekts (insbesondere auf Containertypen wie ein Array oder eine Menge).Standard-Sprachwerkzeuge
Erstellen Sie eine Art
Array :
let numbersArray = [0, 1, 2]
... und dann in einem
Zyklus durchgehen:
for number in numbersArray { print(number) }
... scheint eine sehr natürliche Handlung zu sein, insbesondere für moderne Programmiersprachen wie
Swift . Hinter den Kulissen dieser einfachen Aktion befindet sich jedoch Code, der die Prinzipien des Iteratormusters implementiert.
In "Swift" muss der Variablentyp
das Sequence
implementieren, um eine Variable mit
for
-cycles "iterieren" zu können. Für dieses Protokoll muss dem Typ unter anderem ein
Iterator
vom Typ Typ
associatedtype
, der wiederum die Anforderungen des
IteratorProtocol
Protokolls implementieren muss, sowie die
Factory-Methode makeIterator()
, die einen bestimmten "Iterator" für diesen Typ zurückgibt:
protocol Sequence { associatedtype Iterator : IteratorProtocol func makeIterator() -> Self.Iterator
Das
IteratorProtocol
Protokoll enthält wiederum nur eine Methode -
next()
, die das nächste Element in der Sequenz zurückgibt:
protocol IteratorProtocol { associatedtype Element mutating func next() -> Self.Element? }
Es klingt wie "viel komplizierter Code", ist es aber eigentlich nicht. Unten werden wir das sehen.
Der
Array
Typ implementiert das
Sequence
(allerdings nicht direkt, sondern über die
Protokollvererbungskette : Die
MutableCollection
erbt die
Collection
Anforderungen und letztere die
Sequence
Anforderungen), sodass insbesondere seine Instanzen mit
for
-cycles „iteriert“ werden können.
Benutzerdefinierte Typen
Was muss getan werden, um Ihren eigenen Typ iterieren zu können? Wie so oft ist es am einfachsten, ein Beispiel zu zeigen.
Angenommen, es gibt einen Typ, der ein Bücherregal darstellt, in dem bestimmte Instanzen einer Klasse gespeichert sind, die wiederum ein Buch darstellen:
struct Book { let author: String let title: String } struct Shelf { var books: [Book] }
Um eine Instanz der
Shelf
Klasse "iterieren" zu können, muss diese Klasse die Anforderungen des
Sequence
erfüllen. In diesem Beispiel reicht es aus, nur die Methode
makeIterator()
zu implementieren, zumal die anderen Protokollanforderungen
Standardimplementierungen haben. Diese Methode sollte eine Instanz eines Typs zurückgeben, der das
IteratorProtocol
Protokoll implementiert. Glücklicherweise ist dies im Fall von Swift sehr wenig sehr einfacher Code:
struct ShelfIterator: IteratorProtocol { private var books: [Book] init(books: [Book]) { self.books = books } mutating func next() -> Book? {
Die
next()
-Methode vom Typ
ShelfIterator
als
mutating
deklariert, da die
ShelfIterator
den
ShelfIterator
speichern muss, der der aktuellen Iteration entspricht:
mutating func next() -> Book? { defer { if !books.isEmpty { books.removeFirst() } } return books.first }
Diese Implementierungsoption gibt immer das erste Element in der Sequenz zurück oder
nil
wenn die Sequenz leer ist. Der
defer
mit dem Code zum Ändern der iterierten Auflistung "umbrochen", wodurch das Element des letzten Iterationsschritts unmittelbar nach der Rückkehr der Methode entfernt wird.
Anwendungsbeispiel:
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)") }
Weil Alle verwendeten Typen (einschließlich des
Array
zugrunde liegenden
Shelf
) basieren auf der
Semantik von Werten (im Gegensatz zu Referenzen) . Sie müssen sich keine Sorgen machen, dass der Wert der ursprünglichen Variablen während der Iteration geändert wird. Bei der Behandlung von Typen, die auf der Link-Semantik basieren, sollte dieser Punkt berücksichtigt und beim Erstellen eigener Iteratoren berücksichtigt werden.
Klassische Funktionalität
Der im Buch „Gangs of Four“ beschriebene klassische „Iterator“ kann neben der Rückgabe des nächsten Elements der iterierbaren Sequenz auch jederzeit das aktuelle Element im Iterationsprozess zurückgeben, wobei das erste Element der iterierbaren Sequenz und der Wert des „Flags“ angeben, ob noch vorhanden sind Elemente in einer iterierten Sequenz relativ zum aktuellen Iterationsschritt.
Natürlich wäre es einfach, ein Protokoll zu deklarieren, wodurch die Funktionen des Standard-
IteratorProtocol
:
protocol ClassicIteratorProtocol: IteratorProtocol { var currentItem: Element? { get } var first: Element? { get } var isDone: Bool { get } }
Das erste und das aktuelle Element werden seitdem optional zurückgegeben Die Quellsequenz ist möglicherweise leer.
Einfache Implementierungsoption:
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 } }
In der ursprünglichen Beschreibung des Musters ändert die
next()
-Methode den internen Status des Iterators, um zum nächsten Element zu wechseln, und ist vom Typ
Void
. Das aktuelle Element wird von der
currentElement()
-Methode zurückgegeben. Im
IteratorProtocol
Protokoll sind diese beiden Funktionen so, als ob sie zu einer kombiniert würden.
Die Notwendigkeit der
first()
-Methode ist ebenfalls zweifelhaft, weil Der Iterator ändert die ursprüngliche Sequenz nicht und wir haben immer die Möglichkeit, auf sein erstes Element zuzugreifen (falls vorhanden, natürlich).
Und da die
next()
-Methode nach Abschluss der Iteration
nil
zurückgibt, wird auch die
isDone()
-Methode unbrauchbar.
Für akademische Zwecke ist es jedoch durchaus möglich, eine Funktion zu entwickeln, die die volle Funktionalität nutzen kann:
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)
Der
iterator
Parameter wird als
inout
deklariert, weil Sein interner Zustand ändert sich während der Ausführung der Funktion. Und wenn die Funktion aufgerufen wird, wird die Iteratorinstanz nicht direkt durch ihren eigenen Wert übertragen, sondern als Referenz.
Das Ergebnis des Aufrufs der
next()
-Methode wird nicht verwendet, wodurch das Fehlen des Rückgabewerts einer Lehrbuchimplementierung simuliert wird.
Anstelle einer Schlussfolgerung
Dies scheint alles zu sein, was ich diesmal sagen wollte. Alles schöne Code und absichtliches Schreiben!
Meine anderen Artikel zu Designmustern: