handbookНаписать

patterns/behavioral/iterator/README.md · тема · 4 языка · 19 тестов

+ практика: постраничный обход API заказов

Итератор (Iterator) ​

Даёт обойти коллекцию по одному элементу, не раскрывая, как она устроена внутри.

Проблема ​

Каталог магазина — дерево категорий: «Каталог» → «Электроника» → «Телефоны» и так далее. Меню нужно показать его в глубину (раздел, затем его подразделы), поиску — по уровням (сначала верхние категории, потом вложенные). Если каждый клиент обходит дерево сам, он знает, что у категории есть поле children, держит свой стек или очередь и повторяет один и тот же цикл в пяти местах. Стоит перенести дерево в плоскую таблицу с parent_id, и правки расползаются по всему коду.

Клиенту на самом деле нужно одно: получить категории по очереди и, может быть, остановиться на нужной.

Решение ​

Коллекция CategoryTree сама отдаёт обходы. Обход по умолчанию — в глубину: дерево можно сразу передать в foreach / for ... of / range. Обход в ширину — отдельный метод breadthFirst, который возвращает итератор BreadthFirstIterator. Клиент видит только «что-то, по чему можно пройти циклом», а стек, очередь и поле children остаются внутри.

Положение обхода хранит итератор, а не коллекция. Поэтому два обхода одного дерева идут независимо, и коллекции не нужны поля вроде currentIndex.

Структура ​

root

creates

iterates

Category

+name: string

+children: Category[]

CategoryTree

-root: Category

+iterate() : Iterator

+depthFirst() : Iterator

+breadthFirst() : Iterator

BreadthFirstIterator

-queue: Category[]

+next() : Category

Client

names(iterable) : string[]

Коллекция — это неизменяемая Category и обёртка CategoryTree, которая наружу отдаёт только обходы. Обход в глубину написан генератором: цикл со стеком, где каждый yield отдаёт категорию и замораживает функцию до следующего запроса. Дети кладутся в стек в обратном порядке, чтобы первый ребёнок вышел первым. Генератор — тот же итератор, только его состояние (стек и место остановки) за нас хранит язык.

Как это собирается ​

Дерево собирает слой загрузки каталога: из таблицы категорий приходят плоские строки "id, родитель, имя", и функция сборки рекурсивно складывает из них Category, а корень заворачивает в CategoryTree. Строка с несуществующим родителем (Toys ссылается на id 9) в дерево не попадает: загрузчик собирает такие строки в список "orphans", чтобы показать их в админке, а не терять молча.

Дальше каждая страница берёт свой обход и ничего не знает об устройстве дерева. Карта сайта проходит всё дерево обычным циклом в глубину. Верхнее меню берёт breadthFirst, пропускает корень и останавливается после трёх пунктов: обход ленивый, поэтому большое дерево до конца не проходится.

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

Итератор обхода в ширину написан руками, как в классическом описании паттерна: объект с очередью необработанных категорий и методом «дай следующую». Его полезно сравнить с генератором из коллекции: разница между обходами — только стек против очереди, а разница между способами записи — кто хранит состояние. В Go отдельного объекта нет: итератор — функция iter.Seq, а «ручной» шаг даёт iter.Pull в тестах.

ЯзыкОсобенность
PHPIteratorAggregate с генератором для foreach; обход в ширину — класс с пятью методами Iterator и SplQueue
Goiter.Seq[Category] и range-over-func (Go 1.23+); обход останавливается, когда yield вернул false; iter.Pull превращает его в пошаговый
Python__iter__ возвращает генератор; обход в ширину — класс с __iter__/__next__ и deque
TypeScript[Symbol.iterator] и генератор *depthFirst; обход в ширину — класс с next(), возвращающий { done, value }

Запуск: make test T=patterns/behavioral/iterator

Плюсы и минусы ​

Тесты прогоняют одну клиентскую функцию names по обоим обходам: она принимает что угодно итерируемое и получает разный порядок — в глубину Catalog, Electronics, Phones, Laptops, Home, Kitchen, в ширину Catalog, Electronics, Home, Phones, Laptops, Kitchen. Третий тест берёт два итератора одного дерева, сдвигает первый и проверяет, что второй остался в начале. Последний тест фиксирует особенность языка: в PHP ручной итератор можно перемотать (rewind) и пройти ещё раз, в Python и TS он одноразовый, а в Go вместо этого проверяется, что break останавливает обход посередине.

  • ✅ Клиент не знает устройства коллекции: дерево можно хранить иначе, клиенты не изменятся.
  • ✅ Разные порядки обхода — разные итераторы, коллекция не растёт условиями.
  • ✅ Несколько обходов одной коллекции идут одновременно и не мешают друг другу.
  • ✅ Обход ленивый: клиент может остановиться, и остаток не вычисляется.
  • ❌ Для массива в памяти это лишний слой: обычный цикл по индексам проще.
  • ❌ Ручной итератор — заметно больше кода, чем генератор, и легко ошибиться в его состоянии.
  • ❌ Если коллекцию меняют во время обхода, итератор видит её непредсказуемо: правила для этого придётся придумать отдельно.

Когда не применять ​

  • Коллекция — обычный массив или список: язык уже даёт итератор, своя обёртка ничего не добавит.
  • Клиенту нужен произвольный доступ (items[42]) или длина без обхода: итератор даёт только «следующий».
  • Над элементами нужно сделать много разных операций, зависящих от типа узла: это скорее Посетитель (Visitor), а обход остаётся итератору.

Отличия между языками ​

  • PHP — foreach понимает два контракта: Iterator (пять методов, состояние в объекте) и IteratorAggregate (вернуть итератор). Генератор реализует Iterator сам, но его нельзя перемотать после начала обхода, поэтому коллекция отдаёт каждый раз новый генератор, а не хранит один.
  • Go — до 1.23 итераторы писали как структуры с Next() (bufio.Scanner, sql.Rows). Теперь стандарт — функция iter.Seq, по которой работает range; объекта-итератора нет вовсе. Если yield вернул false, функция обязана выйти: продолжение после break — паника во время выполнения.
  • Python — протокол из двух методов: __iter__ и __next__, конец — исключение StopIteration. Генератор пишет оба за вас, поэтому ручной класс встречается редко. Итератор одноразовый: второй проход по тому же объекту пуст, а по коллекции — полный, потому что __iter__ создаёт новый.
  • TypeScript — for ... of работает с любым объектом, у которого есть [Symbol.iterator]. Генератор (function*) возвращает объект, который одновременно итератор и итерируемое, как и ручной класс с [Symbol.iterator]() { return this; }.

Практика ​

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

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS