[Part 1 - Introduction, Part 2 - Additional enumerators.]
In first two installments of this series, we took a look at for..in statement, described Delphi support for custom enumerators and demonstrated one possible way to add additional enumerator to a class that already implements one.
Today we'll implement a parameterized enumerator. Enumerators of this kind can be especially handy when used as filters. For example, an enumerator that takes a class reference as a parameter can be used to filter a TObjectList containing objects of many different classes.
IOW, we would like to write a following construct: for obj in list.Enum(TSomeClass) do. Clearly, Enum cannot be a property anymore, but it can be a function.
We can still use an enumerator factory object stored inside our class (as in the Part 2). Besides that, we must implement enumeration function that will take one or more parameters, preserve them inside the factory object and return the factory object so that Delphi compiler can call its GetEnumerator function.
Sounds complicated but in reality it really is simple.
First we have to add an internal factory object and an enumeration function to the TCustomStringList class.
TCustomStringList = class(TStringList)
private
FEnumEveryNth: TCSLEnumEveryNthFactory;
public
constructor Create;
destructor Destroy; override;
function SkipEveryNth(skip: integer): TCSLEnumEveryNthFactory;
end;
constructor TCustomStringList.Create;
begin
inherited;
FEnumEveryNth := TCSLEnumEveryNthFactory.Create(Self);
end;
destructor TCustomStringList.Destroy;
begin
FEnumEveryNth.Free;
inherited;
end;
function TCustomStringList.SkipEveryNth(skip: integer): TCSLEnumEveryNthFactory;
begin
FEnumEveryNth.Skip := skip;
Result := FEnumEveryNth;
end;
Enumeration function SkipEveryNth takes one parameter and passes it to the factory object. Then it returns this factory object.
We also need a new factory class. This is an extended version of factory class from Part 2. It must implement storage for all enumerator parameters. In this case, this storage is implemented via property Skip.
TCSLEnumEveryNthFactory = class
private
FOwner: TCustomStringList;
FSkip: integer;
public
constructor Create(owner: TCustomStringList);
function GetEnumerator: TCSLEnumEveryNth;
property Skip: integer read FSkip write FSkip;
end;
constructor TCSLEnumEveryNthFactory.Create(owner: TCustomStringList);
begin
inherited Create;
FOwner := owner;
FSkip := 1;
end;
function TCSLEnumEveryNthFactory.GetEnumerator: TCSLEnumEveryNth;
begin
Result := TCSLEnumEveryNth.Create(FOwner, FSkip);
end;
As you can see, GetEnumerator was also changed - in addition to passing FOwner to the created enumerator, it also passes current value of the Skip property.
New enumerator is very similar to the one from the Part 2, except that it advances list index by the specified ammount (instead of advancing it by 2).
TCSLEnumEveryNth = class
private
FOwner: TCustomStringList;
FListIndex: integer;
FSkip: integer;
public
constructor Create(owner: TCustomStringList; skip: integer);
function GetCurrent: string;
function MoveNext: boolean;
property Current: string read GetCurrent;
end;
constructor TCSLEnumEveryNth.Create(owner: TCustomStringList; skip: integer);
begin
FOwner := owner;
FListIndex := -skip;
FSkip := skip;
end;
function TCSLEnumEveryNth.GetCurrent: string;
begin
Result := FOwner[FListIndex];
end;
function TCSLEnumEveryNth.MoveNext: boolean;
begin
Inc(FListIndex, FSkip);
Result := (FListIndex < FOwner.Count);
end;
We can now write some test code.
procedure TfrmFunWithEnumerators.btnSkipEveryThirdClick(Sender: TObject);
var
ln: string;
s : string;
begin
ln := '';
for s in FslTest.SkipEveryNth(3) do
ln := ln + s;
lbLog.Items.Add('Parameterized enumerator: ' + ln);
end;
Delphi compiler will translate this for..in roughly into
enumerator := FslTest.SkipEveryNth(3).GetEnumerator;
while enumerator.MoveNext do
ln := ln + enumerator.Current;
enumerator.Free;
So what is going on here?
- FslTest.SkipEveryNth(3) sets FslTest.FEnumEveryNth.Skip to 3 and returns FslTest.FEnumEveryNth.
- Compiler calls FslTest.FEnumEveryNth.GetEnumerator.
- FslTest.FEnumEveryNth,GetEnumerator calls TCSLEnumEveryNth.Create(FslTest, 3) and returns newly created object.
- Enumerator loops until MoveNext returns false.
Test code result:
Tomorrow we'll do something even more interesting - we'll add new enumerator to existing class without creating a derived class.
This is interesting stuff, and don't get me wrong, I'll certainly be looking out for the next installment. But... you need more lines to hook into the compiler's for/in syntax than if you didn't bother! Cf.:
ReplyDeleteICSLEnum = interface
function FindNext(out S: string): Boolean;
end;
TCStringList = class(TStringList)
public
function CreateEnumerator(Skip: Integer): ICSLEnum;
end;
TCSLEnum = class(TInterfacedObject, ICSLEnum)
private
FListIndex: Integer;
FOwner: TCStringList;
FSkip: Integer;
protected
function FindNext(out S: string): Boolean;
public
constructor Create(Owner: TCStringList;
Skip: Integer);
end;
constructor TCSLEnum.Create(Owner: TCStringList; skip: integer);
begin
FOwner := Owner;
FSkip := Skip;
end;
function TCSLEnum.FindNext(out S: string): Boolean;
begin
Result := (FListIndex < FOwner.Count);
if not Result then Exit;
S := FOwner[FListIndex];
Inc(FListIndex, FSkip);
end;
function TCStringList.CreateEnumerator(Skip: Integer): ICSLEnumEveryNth;
begin
Result := TCSLEnum.Create(Self, Skip);
end;
with FslTest.CreateEnumerator(3) do
while FindNext(s) do ln := ln + s;
I've shortened the class names so it looks better in the comment box. Anyhow, note there's no factory class needed, which keeps the overall line count down. Also, what replaces the actual for/in construct takes the same number of lines (i.e., 2) that your original did.
Sure, but that way you cannot use the for..in :)
ReplyDeleteI've always been an advocate of the school that says "write more code when preparing an infrastructure so you can write less code when using this infrastructure"
Very nice stuff, one question I have is will this work with BDS 2007, 8.. or is this an undocumented feature?
ReplyDeletethanks again
Dave N.
This is all fully documented (I even quoted Delphi help in Part 1) and will continue to work in future Delphis.
ReplyDeleteI've always been an advocate of the school that says "write more code when preparing an infrastructure so you can write less code when using this infrastructure"
ReplyDeleteYeah, but in my version, the 'infrastructure' amounts to less code and the 'use' of it amounts to about the same... Anyhow, I'll be looking out now for part 5. You're going to be creating a class helper, I assume...
Of course :)
ReplyDeleteWith plenty of dire warnings.
This is a nice example, except that it isn't thread-safe. In fact, worse than that, it isn't even possible to have two enumerators (with different Skip factors) on the same string list at one time, even on a single thread (a for in loop nested within another for in loop).
ReplyDeleteTo run multiple enumerators on one structure, you have to use enumerator factory and interfaces. See parts 4, 5, and 6 for more information.
ReplyDeleteAs for the thread-safety - threading is hard. My enumerator code doesn't try at all to be multithread-aware.
I don´t see why with this code it shouldn't be possible to have two enumerators (with different Skip factors) on the same string list at one time. The index counter is inside the enumerator and that is instantiated for every single for..in loop, meaning another counter for each loop. Where would be the problem?
ReplyDeleteIs the implementation of the TCSLEnumEveryNth constructor missing the "inherited Create;"? Or is it not needed in this case...
ReplyDeleteAs suggested I made a enumerator that returns the specified class from a collection that supports :-
ReplyDeletefor page in mBook.Filter< cPage > do begin
for picture in page.Filter< cPhotoItem > do begin
implemented :-
cObjNode = class // base class for tree objects
....
private
oMakeEnum : TObject; // cMakeEnumMyList< T >;
function FindMatch< T : class >( index : integer ) : integer;
type // see http://www.thedelphigeek.com/2007/03/fun-with-enumerators-part-3.html Primož Gabrijelčič
cEnumNode< T : class > = class
constructor Create( owner: cObjNode );
private
mOwner : cObjNode;
mIndex : integer;
public
function GetCurrent() : T; // [ index ]
function MoveNext: boolean; // Inc( index ) if index
property Current : T read GetCurrent;
end;
cMakeEnumNode< T : class > = class // as required by 'for in'
constructor Create( owner : cObjNode );
private
mOwner : cObjNode;
public
function GetEnumerator: cEnumNode< T >;
end;
public
function Filter< T : class >() : cObjNode.cMakeEnumNode< T >;
end;
...
function cObjNode.Filter< T >() : cObjNode.cMakeEnumNode< T >;
begin
oMakeEnum.Free; // free the previous one
result := cMakeEnumNode< T >.Create( self );
oMakeEnum := result;
end;
function cObjNode.FindMatch< T >( index : integer ) : integer; // filter logic
begin
result := ChildCount; // default to over the end
while index < ChildCount do begin
if oChildren[ index ] is T then begin
result := index;
break;
end;
Inc( index );
end;
end;
constructor cObjNode.cEnumNode< T >.Create( owner: cObjNode );
begin
mOwner := owner;
mIndex := -1;
end;
function cObjNode.cEnumNode< T >.GetCurrent() : T;
begin
if mIndex < mOwner.ChildCount then result := mOwner[ mIndex ] as T
else result := nil;
end;
function cObjNode.cEnumNode< T >.MoveNext: boolean;
begin
mIndex := mOwner.FindMatch< T >( mIndex + 1 );
result := mIndex < mOwner.ChildCount;
end;
constructor cObjNode.cMakeEnumNode< T >.Create( owner : cObjNode );
begin
mOwner := owner;
end;
function cObjNode.cMakeEnumNode< T >.GetEnumerator: cEnumNode< T >;
begin
result := cObjNode.cEnumNode< T >.Create( mOwner );
end;