Главная » Хабрахабр » Архитектурный шаблон «Итератор» («Iterator») во вселенной «Swift»

Архитектурный шаблон «Итератор» («Iterator») во вселенной «Swift»

«Итератор» – один из шаблонов проектирования, которые программисты чаще всего не замечают, потому что его реализация, как правило, встроена непосредственно в стандартные средства языка программирования. Тем не менее, это тоже один из поведенческих шаблонов, описанных в книге «Банды четырех» (“Gang of Four”, “GoF”) “Шаблоны проектирования” (“Design Patterns: Elements of Reusable Object-Oriented Software”), и понимать его устройство никогда не помешает, а иногда даже может в чем-то помочь.
«Итератор» представляет собой способ последовательного доступа ко всем элементам составного объекта (в частности, контейнерных типов, таких как массив или набор).

Стандартные средства языка

Создать какой-нибудь массив:

let numbersArray = [0, 1, 2]

…а потом «пройтись» по нему циклом:

for number in numbersArray { print(number)
}

…кажется очень естественным действием, особенно для современных языков программирования, таких как «Swift». Тем не менее, за «кулисами» этого простого действия находится код, реализующий принципы шаблона «Итератор».

Помимо прочего, этот протокол требует от типа иметь associatedtype Iterator, который в свою очередь должен реализовывать требования протокола IteratorProtocol, а также фабричный метод makeIterator(), который возвращает конкретный «итератор» для данного типа: В «Swift» для того, чтобы иметь возможность «итерировать» переменную с помощью for-циклов, тип переменной должен реализовывать протокол Sequence.

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 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»


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Древности: невероятная видеокассета

Сейчас, в 2019 году видеокассета потеряла всякую актуальность. Когда год назад я решил оцифровать свои старые записи, и не без труда вывел картинку с видеомагнитофона на современную метровую ЖК-панель, это был опыт, сравнимый с прослушиванием грамофонных пластинок на 78 оборотов. ...

Я прочитал 80 резюме, у меня есть вопросы

У нас не очень простое собеседование. Нужно пройти 3 шага: Прислать резюме, программист его посмотрит, лайкнет если всё хорошо. Рекрутер позвонит, задаст несколько вопросов Встретиться или созвониться с нами. Узнаем, какой вы специалист. Прийти на тестовый день. Познакомиться с командой ...