„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: