вторник, 2 февраля 2010 г.

Использование волокон для упрощения enumerator-ов, часть 2: когда жизнь проще для вызывающего

Это перевод Using fibers to simplify enumerators, part 2: When life is easier for the caller. Автор: Реймонд Чен.

В последний раз мы увидели, как могла бы быть написанной функция перечисления каталога, если бы спецификацию на неё проектировал человек, пишущий перечислитель. Давайте теперь посмотрим, во что она превратится, если спецификацию будет писать человек, её вызывающий.
type
TEnumFound = (
fefFile, // нашли файл
fefDir, // нашли каталог
fefLeaveDir, // вышли из каталога
fefDone // закончили
);

TDirectoryTreeEnumerator = class
private
// ... тут реализация ...
public
constructor Create(const ADir: String);

function Next: TEnumFound;
procedure Skip;

property CurDir: String read GetCurDir;
property CurPath: String read FPath;
property CurFindData: TSearchRec read GetCurFindData;
end;
С этим дизайном, перечислитель выплёвывает файлы, а вызывающий контролирует его, говоря, когда идти дальше, опционально указывая, что какой-то каталог надо бы пропустить.

Заметьте, что у нас теперь нет аналога кода ferStop. Если вызывающий хочет остановить перечисление - он просто прекращает вызывать метод Next.

С этим дизайном наша тестовая функция будет довольно простой:
procedure TForm1.Button1Click(Sender: TObject);

function TestWalk(const AEnum: TDirectoryTreeEnumerator): UInt64;
var
SizeSelf, SizeAll: UInt64;
Found: TEnumFound;
begin
SizeSelf := 0;
SizeAll := 0;
Result := 0;
repeat
Found := AEnum.Next;
case Found of
fefFile:
SizeSelf := SizeSelf + AEnum.CurFindData.Size;

fefDir:
SizeAll := SizeAll + TestWalk(AEnum);

fefLeaveDir:
begin
SizeAll := SizeAll + SizeSelf;
Memo1.Lines.Add(Format('Size of %s is %u (%u)', [AEnum.CurDir, SizeSelf, SizeAll]));
Result := SizeAll;
end;

fefDone:
Result := SizeAll;
end;
until (Found = fefDone) or (Found = fefLeaveDir);
end;

var
Enum: TDirectoryTreeEnumerator;
begin
Memo1.Lines.BeginUpdate;
try
Enum := TDirectoryTreeEnumerator.Create('.');
try
TestWalk(Enum);
finally
FreeAndNil(Enum);
end;
finally
Memo1.Lines.EndUpdate;
end;
end;
Конечно же, этот вариант дизайна нагружает всей реальной работой реализацию перечислителя. Вместо того, чтобы позволить перечислителю пройтись по дереву, вызывая для каждого найденного элемента функцию (callback) - теперь вызывающий постоянно вызывает Next, и каждый раз перечислителю нужно найти следующий файл и вернуть его. Поскольку перечислитель возвращает управление, то он не может хранить своё состояние в стеке; вместо этого ему нужно эмулировать это вручную.
  TDirectoryTreeEnumerator = class
private
type
TEnumState = (
ES_NORMAL,
ES_SKIP,
ES_FIRST
);
PStackEntry = ^TStackEntry;
TStackEntry = record
Next: PStackEntry;
hFind: Boolean;
SR: TSearchRec;
Dir: String;
end;
var
FCur: PStackEntry;
FES: TEnumState;
FPath: String;
function Push(const ADir: String): PStackEntry;
procedure StopDir;
function Stopped: Boolean;
procedure Pop;
function GetCurFindData: TSearchRec;
function GetCurDir: String;
public
constructor Create(const ADir: String);
destructor Destroy; override;

function Next: TEnumFound;
procedure Skip;

property CurDir: String read GetCurDir;
property CurPath: String read FPath;
property CurFindData: TSearchRec read GetCurFindData;
end;

constructor TDirectoryTreeEnumerator.Create(const ADir: String);
begin
Push(ADir);
end;

destructor TDirectoryTreeEnumerator.Destroy;
begin
while Assigned(FCur) do
begin
StopDir;
Pop;
end;
end;

function TDirectoryTreeEnumerator.GetCurDir: String;
begin
Result := FCur^.Dir;
end;

function TDirectoryTreeEnumerator.GetCurFindData: TSearchRec;
begin
Result := FCur^.SR;
end;

function TDirectoryTreeEnumerator.Push(const ADir: String): PStackEntry;
begin
New(Result);
FillChar(Result^, SizeOf(Result^), 0);
FPath := IncludeTrailingPathDelimiter(ADir);
Result^.Dir := FPath;
Result^.hFind := (FindFirst(Result^.Dir + '*.*', faAnyFile, Result^.SR) = 0);
if Result^.hFind then
begin
Result^.Next := FCur;
FES := ES_FIRST;
FCur := Result;
end
else
begin
Dispose(Result);
Result := nil;
end;
end;

procedure TDirectoryTreeEnumerator.Skip;
begin
FES := ES_SKIP;
end;

procedure TDirectoryTreeEnumerator.StopDir;
var
pse: PStackEntry;
begin
pse := FCur;
if pse.hFind then
begin
FindClose(pse.SR);
pse.hFind := False;
end;
end;

function TDirectoryTreeEnumerator.Stopped: Boolean;
begin
Result := not FCur^.hFind;
end;

procedure TDirectoryTreeEnumerator.Pop;
var
pse: PStackEntry;
begin
pse := FCur;
FCur := pse^.Next;
Dispose(pse);
end;

function TDirectoryTreeEnumerator.Next: TEnumFound;
label
Loop;
begin
Loop:
// Есть что-то искать?
if not Assigned(FCur) then
Exit(fefDone);

// Если нам просто выйти - то делаем pop
if Stopped then
begin
Pop;
FES := ES_NORMAL;
end

// Если каталог принят, то входим в него
else
if (FES = ES_NORMAL) and
((FCur^.SR.Attr and faDirectory) <> 0) then
Push(FPath);

// Ещё файлы в этом каталоге?
if (FES <> ES_FIRST) and
(FindNext(FCur.SR) <> 0) then
begin
StopDir;
Exit(fefLeaveDir);
end;

// Не надо заходить в . или ..
if (FCur^.SR.Name = '.') or
(FCur^.SR.Name = '..') then
begin
FES := ES_SKIP;
goto Loop;
end;

FPath := FCur^.Dir + FCur.SR.Name;

// Возвращаем найденный элемент
FES := ES_NORMAL; // Состояние по-умолчанию
if (FCur^.SR.Attr and faDirectory) <> 0 then
Exit(fefDir)
else
Exit(fefFile);
goto Loop;
end;
Тьфу ты, пропасть! Простая рекурсивная функция превратилась в это ужасное месиво кода по управлению состояниями.

Разве не было бы здорово, если бы мы могли взять лучшее от обоих миров? Вызывающий мог бы видеть простой перечислитель, выплёвывающий файлы (или каталоги). А вызываемый видел бы функцию обратного вызова, в которую он засовывал бы файлы.

Мы построим такую штуку в следующий раз.

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

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.