Das Iterator-Architekturmuster im schnellen Universum

„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 // Another requirements go here… } 

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? { // TODO: Return next underlying Book element. } } extension Shelf: Sequence { func makeIterator() -> ShelfIterator { return ShelfIterator(books: books) } } 

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) /* 1. .  –  2. .  –   3. .  –   */ 

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:

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


All Articles