"Iterator" es uno de los patrones de diseño que los programadores a menudo no notan, porque su implementación, como regla, está integrada directamente en las herramientas estándar del lenguaje de programación. Sin embargo, este es también uno de los patrones de comportamiento descritos en el
libro "Gang of Four", "GoF", "Patrones de diseño: elementos de software orientado a objetos reutilizables" , y comprender su dispositivo nunca duele, y a veces incluso puede ayudar en algo.
Un "iterador" es un método de acceso secuencial a todos los elementos de un objeto compuesto (en particular, los tipos de contenedor, como una matriz o conjunto).Herramientas de lenguaje estándar
Crea algún tipo de
matriz :
let numbersArray = [0, 1, 2]
... y luego "caminar" a través de él en un
ciclo :
for number in numbersArray { print(number) }
... parece una acción muy natural, especialmente para lenguajes de programación modernos como
Swift . Sin embargo, detrás de escena de esta acción simple hay un código que implementa los principios del patrón Iterator.
En "Swift", para poder "iterar" una variable usando
for
-cycles, el tipo de variable debe implementar
el protocolo de Sequence
. Entre otras cosas, este protocolo requiere que el tipo tenga un
Iterator
tipo
associatedtype
, que a su vez debe implementar los requisitos del protocolo
IteratorProtocol
, así como el
método de fábrica makeIterator()
, que devuelve un "iterador" específico para este tipo:
protocol Sequence { associatedtype Iterator : IteratorProtocol func makeIterator() -> Self.Iterator
El protocolo
IteratorProtocol
, a su vez, contiene solo un método:
next()
, que devuelve el siguiente elemento en la secuencia:
protocol IteratorProtocol { associatedtype Element mutating func next() -> Self.Element? }
Parece "mucho código complicado", pero en realidad no lo es. A continuación veremos esto.
El tipo
Array
implementa el protocolo de
Sequence
(aunque no directamente, sino a través de la cadena de
herencia del
protocolo :
MutableCollection
hereda los requisitos de la
Collection
, y este último hereda los requisitos de la
Sequence
), por lo que sus instancias, en particular, se pueden "iterar" utilizando
for
-cycles.
Tipos personalizados
¿Qué se debe hacer para poder iterar su propio tipo? Como suele suceder, es más fácil mostrar un ejemplo.
Supongamos que hay un tipo que representa una estantería que almacena un cierto conjunto de instancias de una clase, que a su vez representa un libro:
struct Book { let author: String let title: String } struct Shelf { var books: [Book] }
Para poder "iterar" una instancia de la clase
Shelf
, esta clase debe cumplir los requisitos del protocolo de
Sequence
. Para este ejemplo, será suficiente implementar el método
makeIterator()
, especialmente porque los otros requisitos del protocolo tienen
implementaciones predeterminadas . Este método debería devolver una instancia de un tipo que implementa el protocolo
IteratorProtocol
. Afortunadamente, en el caso de Swift, este es un código muy pequeño y muy simple:
struct ShelfIterator: IteratorProtocol { private var books: [Book] init(books: [Book]) { self.books = books } mutating func next() -> Book? {
El método
next()
del tipo
ShelfIterator
declara
mutating
, porque la instancia de tipo de alguna manera debe almacenar el estado correspondiente a la iteración actual:
mutating func next() -> Book? { defer { if !books.isEmpty { books.removeFirst() } } return books.first }
Esta opción de implementación siempre devuelve el primer elemento de la secuencia, o
nil
si la secuencia está vacía. El bloque de
defer
"envuelve" con el código para cambiar la colección iterada, que elimina el elemento del último paso de iteración inmediatamente después de que el método regrese.
Ejemplo de uso:
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)") }
Porque todos los tipos utilizados (incluido el
Shelf
subyacente de la
Array
) se basan en la
semántica de los valores (a diferencia de las referencias) , no tiene que preocuparse por el cambio del valor de la variable original durante la iteración. Al manejar tipos basados en la semántica de enlaces, este punto debe tenerse en cuenta y tenerse en cuenta al crear sus propios iteradores.
Funcionalidad clásica
El clásico "iterador" descrito en el libro "Gangs of Four", además de devolver el siguiente elemento de la secuencia iterable, también puede devolver en cualquier momento el elemento actual en el proceso de iteración, el primer elemento de la secuencia iterable y el valor de la "bandera" que indica si todavía hay elementos en una secuencia iterada en relación con el paso de iteración actual.
Por supuesto, sería fácil declarar un protocolo ampliando así las capacidades del protocolo de
IteratorProtocol
estándar:
protocol ClassicIteratorProtocol: IteratorProtocol { var currentItem: Element? { get } var first: Element? { get } var isDone: Bool { get } }
Los elementos primero y actual se devuelven opcionales ya que la secuencia fuente puede estar vacía.
Opción de implementación 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 } }
En la descripción original del patrón, el método
next()
cambia el estado interno del iterador para ir al siguiente elemento y es de tipo
Void
, y el elemento actual es devuelto por el método
currentElement()
. En el protocolo
IteratorProtocol
, estas dos funciones se combinan en una sola.
La necesidad del
first()
método
first()
también es dudosa, porque el iterador no cambia la secuencia original, y siempre tenemos la oportunidad de acceder a su primer elemento (si lo hay, por supuesto).
Y, dado que el método
next()
devuelve
nil
cuando finaliza la iteración, el método
isDone()
también se vuelve inútil.
Sin embargo, para fines académicos, es bastante posible encontrar una función que pueda utilizar la funcionalidad completa:
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)
El parámetro
iterator
se declara
inout
porque su estado interno cambia durante la ejecución de la función. Y cuando se llama a la función, la instancia del iterador se transmite no directamente por su propio valor, sino por referencia.
El resultado de llamar al método
next()
no se utiliza, simulando la ausencia del valor de retorno de una implementación de libro de texto.
En lugar de una conclusión
Esto parece ser todo lo que quería decir esta vez. Todo hermoso código y escritura deliberada!
Mis otros artículos sobre patrones de diseño: