Tuesday, March 06, 2007

Fun with enumerators, part 2 - multiple enumerators

In the first installment of this series, we just repeated some well-known facts about the for..in language construct and the implementation of enumerators. Today I'll show a way to implement additional enumerators for a class.

To start, let's write some test framework. Instead of creating a new class implementing an enumerator, we'll just inherit from the TStringList. That way, we get existing TStringList enumerator 'for free'.

TCustomStringList = class(TStringList)
end;


To make things simple, an instance of this class is initialized with one-letter strings from 'a' to 'z' in form's OnCreate handler.

procedure TfrmFunWithEnumerators.FormCreate(Sender: TObject);
var
ch: char;
begin
FslTest := TCustomStringList.Create;
for ch := 'a' to 'z' do
FslTest.Add(ch);
end;

procedure TfrmFunWithEnumerators.FormDestroy(Sender: TObject);
begin
FslTest.Free;
end;


To test it, we'll iterate over the list and collect all lines into a single line.

procedure TfrmFunWithEnumerators.btnStandardIteratorClick(Sender: TObject);
var
ln: string;
s : string;
begin
ln := '';
for s in FslTest do
ln := ln + s;
lbLog.Items.Add('Standard enumerator: ' + ln);
end;


And here's a result of this test code:



Standard enumerator



All good, all well, all very trivial. But our mission was not to test existing enumerator, but to add a new one. So let's take a look at the pseudo-code describing the inner working of the for..in implementation.

enumerator := list.GetEnumerator;
while enumerator.MoveNext do
//do something with enumerator.Current;
enumerator.Free;


The important fact is that enumerator is created directly from the parameter that is passed to the for..in construct (list, in our case). After that, this parameter is not needed anymore and for..in only operates on the enumerator.



To get a new behaviour, we have to provide a different enumerator. To do that, we have to pass a different object to the for..in loop. This object has to provide a GetEnumerator function, which the compiler will use to implement for..in.



In short, we need a factory class that will generate new enumerator and a property inside our main class that will provide this factory to the for..in loop.

  TCSLEnumEverySecondFactory = class
private
FOwner: TCustomStringList;
public
constructor Create(owner: TCustomStringList);
function GetEnumerator: TCSLEnumEverySecond;
end;

TCustomStringList = class(TStringList)
private
FEnumEverySecond: TCSLEnumEverySecondFactory;
public
constructor Create;
destructor Destroy; override;
property SkipEverySecond: TCSLEnumEverySecondFactory read FEnumEverySecond;
end;


Now we can write

procedure TfrmFunWithEnumerators.btnSkipEverySecondClick(Sender: TObject);
var
ln: string;
s : string;
begin
ln := '';
for s in FslTest.SkipEverySecond do
ln := ln + s;
lbLog.Items.Add('Additional enumerator: ' + ln);
end;


and Delphi compiler will translate it (approximately) into

enumerator := FslTest.SkipEverySecond.GetEnumerator;
while enumerator.MoveNext do
ln := ln + enumerator.Current;
enumerator.Free;


We have a winner! That's exactly what we wanted to achieve.



The other parts of the puzzle are simple. The SkipEverySecond factory can be created in the TCustomStringList constructor. We have to pass it Self object (for the reason we'll come to in a minute).

constructor TCustomStringList.Create;
begin
inherited;
FEnumEverySecond := TCSLEnumEverySecondFactory.Create(Self);
end;

destructor TCustomStringList.Destroy;
begin
FEnumEverySecond.Free;
inherited;
end;


The factory class is simple. It has to remember the owner (TCustomStringList) that was passed to the constructor so that it can pass it on to the enumerator whenever compiler requests one.

constructor TCSLEnumEverySecondFactory.Create(owner: TCustomStringList);
begin
inherited Create;
FOwner := owner;
end;

function TCSLEnumEverySecondFactory.GetEnumerator: TCSLEnumEverySecond;
begin
Result := TCSLEnumEverySecond.Create(FOwner);
end;


The enumerator is just a variation on a standard string list enumerator - it increments list index by 2 instead of 1 and skips every second item in the list. Yes, there is no rule saying that enumerator must return all entries in the enumerated object. In fact, you can be very creative inside the enumerator. We'll return to it some other day.

constructor TCSLEnumEverySecond.Create(owner: TCustomStringList);
begin
FOwner := owner;
FListIndex := -2;
end;

function TCSLEnumEverySecond.GetCurrent: string;
begin
Result := FOwner[FListIndex];
end;

function TCSLEnumEverySecond.MoveNext: boolean;
begin
Inc(FListIndex, 2);
Result := (FListIndex < FOwner.Count);
end;


And the results are ...





That wasn't so hard, wasn't it? Tomorrow we'll look into an even niftier topic - enumerators with parameters.



Technorati tags: , ,

3 comments:

  1. Very nice post. :)

    ReplyDelete
  2. I just barely scratched the surface. Stay tuned!

    ReplyDelete
  3. It is nice... but there is a neat alternative approach.

    Instead of having a separate Factory class, you can have the enumerator define its own getEnumerator and return self.

    ie:

    function TMyEnumerator.GetEnumerator:TMyEnumerator;
    begin
    result:=self;
    end;

    Saves having a factory class cluttering up code and needing to be tidied up after.

    ReplyDelete