«Итератор» – один из шаблонов проектирования, которые программисты чаще всего не замечают, потому что его реализация, как правило, встроена непосредственно в стандартные средства языка программирования. Тем не менее, это тоже один из поведенческих шаблонов, описанных в книге «Банды четырех» (“Gang of Four”, “GoF”) “Шаблоны проектирования” (“Design Patterns: Elements of Reusable Object-Oriented Software”), и понимать его устройство никогда не помешает, а иногда даже может в чем-то помочь.
«Итератор» представляет собой способ последовательного доступа ко всем элементам составного объекта (в частности, контейнерных типов, таких как массив или набор).
Стандартные средства языка
Создать какой-нибудь массив:
let numbersArray = [0, 1, 2]
…а потом «пройтись» по нему циклом:
for number in numbersArray {
print(number)
}
…кажется очень естественным действием, особенно для современных языков программирования, таких как «Swift». Тем не менее, за «кулисами» этого простого действия находится код, реализующий принципы шаблона «Итератор».
В «Swift» для того, чтобы иметь возможность «итерировать» переменную с помощью for
-циклов, тип переменной должен реализовывать протокол Sequence
. Помимо прочего, этот протокол требует от типа иметь associatedtype
Iterator
, который в свою очередь должен реализовывать требования протокола IteratorProtocol
, а также фабричный метод makeIterator()
, который возвращает конкретный «итератор» для данного типа:
protocol Sequence {
associatedtype Iterator : IteratorProtocol
func makeIterator() -> Self.Iterator
// Another requirements go here…
}
Протокол IteratorProtocol
в свою очередь содержит в себе всего один метод – next()
, который возвращает следующий элемент в последовательности:
protocol IteratorProtocol {
associatedtype Element
mutating func next() -> Self.Element?
}
Звучит как «много сложного кода», но на самом деле это не так. Чуть ниже мы в этом убедимся.
Тип Array
реализовывает протокол Sequence
(правда, не напрямую, а через цепочку наследования протоколов: MutableCollection
наследует требования Collection
, а тот – требования Sequence
), поэтому его экземпляры, в частности, могут быть «итерированы» с помощью for
-циклов.
Пользовательские типы
Что необходимо сделать, чтобы смочь итерировать свой собственный тип? Как это часто бывает, проще всего показать на примере.
Допустим, имеется тип, представляющий книжную полку, который хранит в себе некий набор экземпляров класса, представляющего в свою очередь книгу:
struct Book {
let author: String
let title: String
}
struct Shelf {
var books: [Book]
}
Чтобы иметь возможность «итерировать» экземпляр класса Shelf
, этот класс должен соответствовать требованиям протокола Sequence
. Для данного примера будет достаточно лишь реализовать метод makeIterator()
, тем более что остальные требования протокола имеют реализации по умолчанию. Этот метод должен вернуть экземпляр типа, реализующего протокол IteratorProtocol
. К счастью, в случае со «Swift» это очень мало очень простого кода:
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)
}
}
Метод next()
типа ShelfIterator
объявлен mutating
, потому что экземпляр типа должен тем или иным образом хранить в себе состояние, соответствующее текущей итерации:
mutating func next() -> Book? {
defer {
if !books.isEmpty { books.removeFirst() }
}
return books.first
}
Данный вариант реализации всегда возвращает первый элемент в последовательности либо nil
, если последовательность пуста. В блок defer
«обернут» код изменения итерируемой коллекции, который удаляет элемент последнего шага итерации сразу после возврата метода.
Пример использования:
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)")
}
/*
Ф. Достоевский – Идиот
Ф. Достоевский – Братья Карамазовы
Ф. Достоевский – Бедные люди
*/
Т.к. все используемые типы (включая Array
, лежащий в основе Shelf
) базируются на семантике значений (в противовес ссылкам), можно не беспокоиться о том, что значение исходной переменной будет изменено в процессе итерации. При обращении с типами, основанными на семантике ссылок, этот момент стоит иметь в виду и учитывать при создании собственных итераторов.
Классический функционал
Классический «итератор», описанный в книге «Банды четырех», помимо того, чтобы возвращать следующий элемент итерируемой последовательности, может также в любой момент возвращать текущий элемент в процессе итерации, первый элемент итерируемой последовательности и значение «флага», показывающего, остались ли еще элементы в итерируемой последовательности относительно текущего шага итерации.
Конечно, было бы несложно объявить протокол, расширяющий таким образом возможности стандартного IteratorProtocol
:
protocol ClassicIteratorProtocol: IteratorProtocol {
var currentItem: Element? { get }
var first: Element? { get }
var isDone: Bool { get }
}
Первый и текущий элементы возвращается опциональными, т.к. исходная последовательность может быть пуста.
Вариант простой реализации:
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
}
}
В оригинальном описании паттерна метод next()
изменяет внутреннее состояние итератора для перехода к следующему элементу и имеет тип Void
, а текущий элемент возвращается методом currentElement()
. В протоколе IteratorProtocol
эти две функции как бы объединены в одну.
Нужда в методе first()
также сомнительна, т.к. итератор не изменяет исходную последовательность, и у нас всегда есть возможность обратиться к ее первому элементу (при его наличии, конечно).
И, так как метод next()
возвращает nil
, когда итерация окончена, метод isDone()
также становится бесполезным.
Тем не менее, в академических целях, вполне возможно придумать функцию, которая смогла бы использовать полный функционал:
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. Ф. Достоевский – Бедные люди
*/
Параметр iterator
объявлен inout
, т.к. его внутреннее состояние меняется в процессе выполнения функции. И при вызове функции экземпляр итератора передается не напрямую собственным значением, а ссылкой.
Результат вызова метода next()
не используется, имитируя отсутствие возвращаемого значения хрестоматийной реализации.
Вместо заключения
Кажется, это все, что мне хотелось сказать в этот раз. Всем красивого кода и осознанного его написания!
Другие мои статьи о шаблонах проектирования:
Архитектурный шаблон «Посетитель» (“Visitor”) во вселенной «iOS» и «Swift»
Автор: Никита Лазарев-Зубов