Судя по всему, термин "лаконичные итераторы" еще особенно не распространен, поэтому я гордо буду считать себя его изобретателем в Delphi'йской среде :-).
О чем речь
Есть такой известный паттерн – итератор: структура, которая обходит содержимое какого-нибудь контейнера и позволяет с ним, содержимым, что-то делать.
Во многих языках поддержка итераторов встроена в синтаксис, даже в Delphi 2005 появилась. В других случаях его приходится писать руками. Хотя, "приходится" - не то слово. Можно итераторами и не пользоваться, а обходить контейнеры простым For i:=0 To Count-1 Do. Но есть, например, такая проблема: в этом случае в процессе прохода нельзя удалять элементы контейнера. Если нужно удалить, приходится писать While с проверкой в духе "если <условие>, то удаляем и не двигаемся к следующему, иначе - двигаемся к следующему". Совсем плохо, если объект может захотеть удалить себя сам при вызове какого-нибудь невинного внешне метода.
Вот, чтобы не писать свою логику обхода каждый раз, ее и запихивают в единый интерфейс итератора.
Итак, классический интерфейс итератора, который советуют всякие мудрые дядьки, выглядит (в делфийском коде) примерно так:
TIterator=Class
Procedure First;Virtual;Abstract;
Function Current:TObject;Virtual;Abstract;
Procedure Next;Virtual;Abstract;
Function Done:Boolean;Virtual;Abstract;
End;{TIterator}
(разумеется, при создании своих итераторов вместо TObject будет что-то более полезное).
Используется это примерно так:
Var
Iterator:TIterator;
Begin
Iterator:=Container.CreateIterator;
Try
Iterator.First;
While Not Iterator.Done Do
Begin
Iterator.Current.SomeMethod;
Iterator.Next;
End;{While}
Finally
Iterator.Free;
End;{Try}
Лаконичный итератор
Конструкция выглядит монструозно и прямо напрашивается на упрощение. Встречаем "лаконичный итератор":
IIterator=Interface
Function HasNextItem:Boolean;
Function Item:TObject;
End;{IIterator}
Используется так:
With Container.Items Do
While HasNextItem Do
Item.SomeMethod;
Гораздо красивее, на мой взгляд :-).
Как и почему оно работает
-
Вместо базового объекта с абстрактными методами используется
interface. Это хорошо по двум причинам. Во-первых, функциональность итератора можно придать любому объекту, а иначе пришлось бы обязательно наследовать его отTIterator, что невозможно для уже существующих объектов со своей иерархией наследования. Во-вторых, для интерфейсов в Delphi делается автоматический подсчет ссылок, и можно не заботиться об удалении итератора из памяти: Delphi удалит его автоматически при выходе из цикла, что тут же избавляет от громоздкогоTry..Finally. -
Весь обход делается функцией
HasNextItem, она двигает итератор на следующий элемент и возвращаетTrue, если он есть иFalse- если достигнут конец списка. При создании итератор указывает "в никуда", но таким образом, чтоHasNextItemдвинет его на первый элемент. ФункцииFirstнет, что означает, что итератор нельзя использовать повторно, но практика показывает, что в подавляющем большинстве случаев это и не нужно. Если нужно, можно создать итератор заново. ФункцииDoneнет, потому что ее роль исполняет все та жеHasNextItem. -
Контейнер сам создает для себя итератор функцией
Items:IIterator. При вызове этой функции создается всегда новый итератор. Это дает возможность, например, построить итератор, разными копиями которого можно будет пользоваться в параллельных потоках. Кроме того, контейнер может иметь несколько итераторов с разными смыслами (обход в случайном порядке, обход только отдельных как-то почмеченых элементов и т.д.)
Попутно это означает, что нельзя написать так:
While Container.Items.HasNextChild Do
Item.SomeMethod;
потому что тогда итераторы будут плодиться бесконечно на каждом повторении цикла. В обход можно использовать полезный паскалевский оператор With.
Вот это и есть, в общем и целом, смысл этих самых лаконичных итераторов. Однако, если вы еще не устали, то дальше есть еще кое-что интересное.
Модификация для пост-проверок
Итераторы можно (и нужно) использовать для поиска элемента в контейнере:
With Container.Items Do
While HasNextItem And Not <Условие Поиска> Do
{Nothing};
Заковыка состоит в том, что после выхода из цикла надо определить, дошли ли мы до конца. HasNextItem для этого не подходит, потому что сама двигает указатель. И тогда, если найденный элемент - последний, она двинется вперед и вернет False, хотя он и был найден. Для таких проверок в интерфейс итератора добавляется еще одна функция:
IIterator=Interface
Function HasNextItem:Boolean;
Function Item:TObject;
Function Done:Boolean;
End;{IIterator}
Функция Done возвращает True, если достигнут конец списка, False - если нет. Но при этом, сама указатель не двигает. В итоге, действие "найти элемент и сделать нечто" выглядит так:
With Container.Items Do
Begin
While HasNextItem And Not <Условие Поиска> Do
{Nothing};
If Not Done Then
Item.DoSomeAction;
End;{With}
Заодно замечу, что новая функция не сильно удорожает создание итератора, потому что проверка на конец контейнера, которая была в HasNextItem, переносится в Done, а HasNextItem вырождается в "двинуть вперед, вернуть Not Done"
Минусы
Минусы, в основном, состоят в нарушени принятой семантики языка:
- функция, создающая итератор -
Items- не дает понять, что она создает новый объект. Ее можно было бы назватьCreateItemsв соответствии с делфийскими соглашениями, но это, честно, выглядело бы менее круто :-). Выбор этот вполне сознательный, потому что чаще всего в одном фрагменте кода используется только один итератор. И, опять же, не надо заботиться о его удалении - функция
HasNextItemменяет состояние объекта, что для функций совсем не принято. Причины такого выбора: опять же возможность написать более короткий код - ну и напоследок, поиск элемента в конструкции "
While HasNextItem And Not <Условие> Do" будет работать только с включенным неполным вычислением булевских выражений. Но эта беда тоже небольшая, так как это дефолтное и давно принятое поведение.
Комментарии: 2
Может кому пригодится... :) Я обычно использую следующую конструкцию, позволяющую спокойно удалять элементы:
For i:=Count-1 downto 0 Do
А почу неупомянуты существующие библиотеки с использованием итераторов? Там реализованы коитейнеры для всех типов. Я например использую аналог STL и есть куча других.