Итак, итераторы. Начиная, кажется, с версии 2007, в дополнение ко стандартному "for", Delphi поддерживается следующий синтаксис...следующий синтаксис:
for element in collection do command;
type
TConnectionFlag = (cfOpen, cfClientLoggedIn, cfServingRequest, cfSupportsKeepalive, cfRequiresKeepalive);
TConnectionFlags = set of TConnectionFlag;
//Можно было сделать проще, с помощью RTTI, но пока обойдёмся так.
function FlagName(flag: TConnectionFlag): string;
begin
case flag of
cfOpen: Result := 'OPEN';
cfClientLoggedIn: Result := 'LOGGED_IN';
cfServingRequest: Result := 'SERVING_REQUEST';
cfSupportsKeepalive: Result := 'SUPPORTS_KEEPALIVE';
cfRequiresKeepalive: Result := 'REQUIRES_KEEPALIVE';
else
Result := 'UNKNOWN FLAG';
end;
end;
procedure PrintConnectionFlags(flags: TConnectionFlags);
var flag: TConnectionFlag;
begin
for flag in flags do
writeln(FlagName(flag));
end;
procedure PrintConnectionFlags(flags: TConnectionFlags);
begin
if cfOpen in flags then writeln(FlagName(cfOpen));
if cfClientLoggedIn in flags then writeln(FlagName(cfClientLoggedIn));
...
end;
Теперь о более интересных возможностях итераторов. Допускается создавать итераторы собственным классам. Например, собственный список или TStringList могут поддерживать итерацию по элементам списка. Большинство стандартных классов её и поддерживают:
var
d: TStringList;
s: string;
begin
d := TStringList.Create;
try
d.LoadFromFile('lines.txt');
for s in d do
writeln(s);
finally
FreeAndNil(d);
end;
end.
Поддержку итерации можно добавить и собственному классу. Для этого нам потребуется вспомогательный класс, итератор (или енумератор, кому как больше нравится). Основной класс должен иметь функцию GetEnumerator, которая создаёт и возвращает экземпляр вспомогательного класса:
type
TMyCollection = class
protected
Items: array of integer;
public
function GetEnumerator: TMyCollectionEnumerator;
end;
function TMyCollection.GetEnumerator: TMyCollectionEnumerator;
begin
Result := TMyCollectionEnumerator.Create(Self);
end;
type
TMyCollectionEnumerator = class
protected
Parent: TMyCollection;
Position: integer;
public
constructor Create(AParent: TMyCollection);
function MoveNext: boolean;
function GetCurrent: integer;
property Current: integer read GetCurrent;
end;
constructor TMyCollectionEnumerator.Create(AParent: TMyCollection);
begin
inherited Create;
Parent := AParent; //сохраняем указатель на объект, который нас создал
Position := -1;
end;
function TMyCollectionEnumerator.MoveNext: boolean;
begin
Result := (Position < High(Parent.Items));
if Result then Inc(Position);
end;
function TMyCollectionEnumerator.GetCurrent: integer;
begin
Result := Parent.Items[Position];
end;
type
TMyCollectionEnumerator = record
Parent: TMyCollection;
Position: integer;
constructor Create(AParent: TMyCollection);
function MoveNext: boolean;
function GetCurrent: integer;
property Current: integer read GetCurrent;
end;
constructor TMyCollectionEnumerator.Create(AParent: TMyCollection);
begin
// inherited Create; //убрано - рекорды не поддерживают наследования
Parent := AParent;
Position := -1;
end;
type
TClass1 = class;
TClass2 = class
MyClass1: TClass1;
end;
TClass1 = class
MyClass2: TClass2;
end;
type
PRecord1 = ^TRecord1;
TRecord2 = record
MyRecord1: PRecord1;
end;
PRecord2 = ^TRecord2;
TRecord1 = record
MyRecord2 = PRecord2;
end;
Очевидный вопрос: а нельзя ли вообще не создавать на итерацию ни объекта, ни рекорда? Нельзя ли просто возвращать в GetEnumerator ссылку на самого себя, если мы уверены, что итерацией будут пользоваться только по очереди?
type
TMyCollection = class
public
function GetEnumerator: TMyCollection;
function MoveNext: boolean;
function GetCurrent: integer;
end;
function TMyCollection.GetEnumerator: TMyCollection;
begin
Result := Self;
end;
Кто-то спросит, можно ли провернуть этот фокус с рекордами. Рекорды ведь не уничтожаются? Да, рекорды не уничтожаются, но с ними такие трюки и не имеют особого смысла. Не забывайте, что рекорды передаются по значению; это значит, что функция GetEnumerator возвращает не ссылку на рекорд, а целый блок данных, всё его содержимое. Вы можете, конечно, вернуть и сам вызываемый объект:
function TMyRecord.GetEnumerator: TMyRecord;
begin
Result := Self;
end;
Блокировки
Это были простые применения итераторов, а теперь перейдём к более сложным. Самое удобное в итераторах то, что они позволяют выполнять произвольный код в момент перебора. Этим мы и воспользуемся. Например, сделаем перебор потоко-безопасным. При работе с несколькими потоками любой программист выполняет перебор примерно так:
EnterCriticalSection(Sync);
try
for i := 0 to Collection.Count - 1 do begin (* do something *) end;
finally
LeaveCriticalSection(Sync);
end;
function TMyCollectionEnumerator.Create(AParent: TMyCollection);
begin
inherited Create; //положим, это класс: у рекордов нет деструкторов
Parent := AParent;
Position := -1;
EnterCriticalSection(Parent.Sync);
end;
TMyCollectionEnumerator.Destroy;
begin
LeaveCriticalSection(Parent.Sync);
inherited;
end;
procedure EnumCollection(Collection: TCollection);
var i: integer;
begin
for i in Collection do begin (* do something *) end; //Collection блокируется автоматически!
end;
function TMyCollection.GetEnumerator: TMyCollectionEnumerator;
begin
EnterCriticalSection(Sync);
try
CopyStateTo(Result); //копирует состояние массива в Result. Мы сможем перебирать result, даже если сам объект к тому времени поменяется
finally
LeaveCriticalSection(Sync);
end;
end;
То, что мы сейчас сделаем - это небольшое колдовство. Дельфи не вызывает деструкторов для рекордов, но она финализирует всё их содержимое, в том числе, вызывает _Release для интерфейсов. Поэтому мы создадим интерфейс, который будет освобождать блокировку по _Release.
Для начала нам потребуется переопределённая реализация IInterface:
type
TMyGatekeeper = class(TObject, IInterface)
protected
Parent: TMyCollection;
RefCnt: integer;
public
constructor Create(AParent: TMyCollection);
//IInterface
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
constructor TMyGatekeeper.Create(AParent: TMyCollection);
begin
inherited Create;
Parent := AParent;
Refcnt := 0;
end;
function TMyGatekeeper.QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
begin
//реализуем стандартно
if GetInterface(IID, Obj) then
Result := 0
else
Result := E_NOINTERFACE;
end;
function TMyGatekeeper._AddRef: Integer; stdcall;
begin
EnterCriticalSection(Parent.Sync);
Result := InterlockedIncrement(RefCnt); //можно было и не interlocked
end;
function TMyGatekeeper._Release: Integer; stdcall;
begin
Result := InterlockedDecrement(RefCnt);
LeaveCriticalSection(Parent.Sync);
//не уничтожаем объект
end;
Теперь сама коллекция:
type
TMyCollectionEnumerator = record
Parent: TMyCollection;
Position: integer;
Gatekeeper: IInterface;
...
end;
TMyCollection = class
protected
Sync: TRtlCriticalSection;
Items: array of integer;
Gatekeeper: TMyGatekeeper;
public
constructor Create;
destructor Destroy; override;
function GetEnumerator: TMyCollectionEnumerator;
end;
constructor TMyCollection.Create;
begin
inherited;
InitializeCriticalSection(Sync);
Gatekeeper := TMyGatekeeper.Create(Self);
end;
destructor TMyCollection.Destroy;
begin
FreeAndNil(Gatekeeper);
DeleteCriticalSection(Sync);
inherited;
end;
function TMyCollection.GetEnumerator: TMyCollectionEnumerator;
begin
Result := TMyCollectionEnumerator.Create(Self);
Result.Gatekeeper := Gatekeeper as IInterface; //коллекция блокируется
end;
Когда у нас спрашивают TMyCollectionEnumerator, мы пользуемся этим свойством: мы возвращаем итератор, внутрь которого кладём переменную типа IInterface. Когда мы помещаем в неё наш Gatekeeper, дельфи автоматически выполняет _AddRef, блокируя коллекцию. Когда рекорд уничтожается, дельфи автоматически финализирует запись, очищает поле Gatekeeper, и, поскольку оно было интерфейсного типа, вызывает ему _Release - и коллекция разблокируется.
Это, несомненно, очень удобный и быстрый способ. Достаточно одного объекта типа Gatekeeper на любую коллекцию; можно использовать его во множестве итераторов сразу. Он создаётся однажды, при создании TMyCollection, и почти не добавляет накладных расходов. Однако здесь есть свои подводные камни. Хотя Delphi гарантирует уничтожение рекорда-итератора, а уничтожая его, гарантирует очистку интерфейса, неизвестно, когда она это сделает. В прилагающемся коде я выполнял некоторые эксперименты, и выяснил, например, что хотя в обычных функциях итератор уничтожается сразу же по выходу из "for ... in", в основном теле консольного приложения итераторы-рекорды не уничтожаются вообще. Так что этот приём следует использовать с осторожностью.
Фильтры
Ещё одно интересное применение итераторов - фильтры. Вместо того, чтобы писать:
for i := 0 to Collection.Length - 1 do
if Collection.Items[i].Connected and Connection.Items[i].LoggedIn and (cfSupportsKeepalive in Connection.Items[i].Flags) then begin
....
end;
for Connection in FilterKeepalive(Connections) do begin
...
end;
type TKeepaliveFilter = record
Parent: TConnections;
function MoveNext: boolean; //выбирает следующий подходящий по фильтрам элемент
...
end;
TKeepaliveFilterFactory = record
Parent: TConnections;
function GetEnumerator: TKeepaliveFilter;
end;
function FilterKeepalive(Parent: TConnections): TKeepaliveFilterFactory;
begin
Result.Parent := Parent;
end;
function TKeepaliveFilterFactory.GetEnumerator: TKeepaliveFilter;
begin
Result := TKeepaliveFilter.Create(Parent);
end;
Другое дело - классы. Здесь никакой дополнительной стоимости не налагается:
type TKeepaliveFilter = class
Parent: TConnections;
function MoveNext: boolean; //выбирает следующий подходящий по фильтрам элемент
...
function GetEnumerator: TKeepaliveFilter;
end;
function FilterKeepalive(Parent: TConnections): TKeepaliveFilter;
begin
Result := TKeepaliveFilter.Create(Parent);
end;
function TKeepaliveFilterCreator.GetEnumerator: TKeepaliveFilter;
begin
Result := Self;
end;
Генераторы
Ещё одно интересное применение итераторов связано с тем, что нам вовсе не обязательно перебирать уже существующие объекты. Итератор может перебирать элементы, вычисляемые им же на ходу. По ссылке в примерах есть генерация чисел фибоначчи, а мы решим более практическую задачу - и более быстро (уж разумеется, без классов-фабрик и интерфейсов, упаси меня господи).
Создадим итератор, который будет возвращать нам все окна старшего уровня в системе:
for WindowHandle in TopLevelWindows do
ShowWindow(WindowHandle, SW_HIDE); //жаль, но десктоп скрыть не получится - он это игнорирует.
type
TWindowEnumerator = record
Handles: array of HWND;
Position: integer;
function MoveNext: boolean;
function GetCurrent: integer;
property Current: integer read GetCurrent;
end;
PWindowEnumerator = ^TWindowEnumerator;
type
TWindowEnumeratorFactory = record
function GetEnumerator: TWindowEnumerator;
end;
function EnumWindowsProc(hwnd: HWND; lParam: LPARAM): BOOL; stdcall;
begin
with PWindowEnumerator(lParam)^ do begin
SetLength(Handles, Length(Handles)+1); //в реальной жизни, конечно, лучше приращивать блоками по 10-15 элементов
Handles[Length(Handles)-1] := hwnd;
end;
Result := true;
end;
function TWindowEnumeratorFactory.GetEnumerator: TWindowEnumerator;
begin
EnumWindows(@EnumWindowsProc, integer(@Result));
Result.Position := -1;
end;
P.S. Вообще говоря, с окнами можно было и не извращаться. Обычные массивы работают ничуть не хуже:
type
THwndArray = array of HWND;
PHwndArray = ^THwndArray;
function EnumWindowsProc(hwnd: HWND; lParam: LPARAM): BOOL; stdcall;
var Handles: PHwndArray;
begin
Handles := PHwndArray(lParam);
SetLength(Handles^, Length(Handles^)+1);
Handles^[Length(Handles^)-1] := hwnd;
Result := true;
end;
function TWindowEnumeratorFactory.GetEnumerator: THwndArray;
begin
EnumWindows(@EnumWindowsProc, integer(@Result));
end;
procedure HideTopLevelWindows;
begin
for WindowHandle in TopLevelWindows do //точно так же
ShowWindow(WindowHandle, SW_HIDE);
end;
for TargetFile in EnumFiles('C:\Windows\System32\*.exe') do
InfectFile(TargetFile);
Заключение
В качестве приложения даю код четырёх маленьких консольных программ, иллюстрирующих кое-что из вышесказанного. Для компиляции требуется Delphi 2010, который вы можете скачать на 30 дней с сайта embarcaderro. Может быть, скомпилится и на прежних версиях, не проверял.
И ещё немного материалов:
Документация на сайте борланд.
Большая статья на английском с примерами генераторов, внешних и внутренних фильтров.
Генераторы-рекорды с подробным сравнением получающегося ассемблерного кода.