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

Гораздо красивее, на мой взгляд :-).

Как и почему оно работает

  1. Вместо базового объекта с абстрактными методами используется interface. Это хорошо по двум причинам. Во-первых, функциональность итератора можно придать любому объекту, а иначе пришлось бы обязательно наследовать его от TIterator, что невозможно для уже существующих объектов со своей иерархией наследования. Во-вторых, для интерфейсов в Delphi делается автоматический подсчет ссылок, и можно не заботиться об удалении итератора из памяти: Delphi удалит его автоматически при выходе из цикла, что тут же избавляет от громоздкого Try..Finally.

  2. Весь обход делается функцией HasNextItem, она двигает итератор на следующий элемент и возвращает True, если он есть и False - если достигнут конец списка. При создании итератор указывает "в никуда", но таким образом, что HasNextItem двинет его на первый элемент. Функции First нет, что означает, что итератор нельзя использовать повторно, но практика показывает, что в подавляющем большинстве случаев это и не нужно. Если нужно, можно создать итератор заново. Функции Done нет, потому что ее роль исполняет все та же HasNextItem.

  3. Контейнер сам создает для себя итератор функцией 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"

Минусы

Минусы, в основном, состоят в нарушени принятой семантики языка:

Комментарии: 2

  1. Сергей Качалов

    Можно итераторами и не пользоваться, а обходить контейнеры простым For i:=0 To Count-1 Do. Но есть, например, такая проблема: в этом случае в процессе прохода нельзя удалять элементы контейнера. Если нужно удалить, приходится писать While с проверкой в духе “если , то удаляем и не двигаемся к следующему, иначе - двигаемся к следующему”.

    Может кому пригодится... :) Я обычно использую следующую конструкцию, позволяющую спокойно удалять элементы:

    For i:=Count-1 downto 0 Do

  2. Erik

    А почу неупомянуты существующие библиотеки с использованием итераторов? Там реализованы коитейнеры для всех типов. Я например использую аналог STL и есть куча других.

Добавить комментарий