Судя по всему, термин "лаконичные итераторы" еще особенно не распространен, поэтому я гордо буду считать себя его изобретателем в 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 и есть куча других.