I’ve been playing around with Anonymous Methods in Delphi 2009 a little bit lately, and I thought one of my experiments might be worth sharing.
I decided I would try to extend TList<T> so that when you enumerate over it in a for..in loop, not every item would be returned. Specifically, only items that passed a filter criteria would be returned, and I would use an anonymous method to specify the filter criteria.
This actually turned out to be kind of fun, as I got to play with Enumerators, Generics and Anonymous Methods all in one fairly short piece of code.
So, first I looked at TList<T>. I was hoping there was some way I could specify that it should use a different Enumerator than the default, but unfortunately I could not find a way. So I descended from TList<T> to create a TFilteredList<T> to:
- add a couple of methods, ClearFilter and SetFilter, which I’ll come back to later
- define TFilteredEnumerator<T>
- reintroduce the GetEnumerator method to create an instance of TFilteredEnumerator<T> instead of TList<T>’s standard TEnumerator<T>
I also declared an anonymous method type called TFilterFunction<T> which takes a parameter of type T and returns true or false depending on whether the parameter passed or failed the filter criteria respectively.
Here’s the definition:
TFilterFunction = reference to function(Item : T) : boolean;
TFilteredList = class(TList)
private
FFilterFunction: TFilterFunction;
public
type
TFilteredEnumerator = class(TEnumerator)
private
FList: TFilteredList;
FIndex: Integer;
FFilterFunction : TFilterFunction;
function GetCurrent: T;
protected
function DoGetCurrent: T; override;
function DoMoveNext: Boolean; override;
function IsLast : Boolean;
function IsEOL : Boolean;
function ShouldIncludeItem : Boolean;
public
constructor Create(AList: TFilteredList;
AFilterFunction : TFilterFunction);
property Current: T read GetCurrent;
function MoveNext: Boolean;
end;
function GetEnumerator: TFilteredEnumerator; reintroduce;
procedure SetFilter(AFilterFunction : TFilterFunction);
procedure ClearFilter;
end;
Most of the methods on TFilteredEnumerator<T> are not terribly interesting. The constructor takes a reference to an instance of our TFilterFunction<T> anonymous method, which it stores in the FFilterFunction field. This constructor gets called from the aforementioned GetEnumerator method of TFilteredList<T>.
Most of the hard work is done by the MoveNext method, which looks like this:
function TFilteredList.TFilteredEnumerator.MoveNext: Boolean;
begin
if IsLast then
Exit(False);
repeat
Inc(FIndex);
until isEOL or ShouldIncludeItem;
Result := not IsEol;
end;
It is invoked when the Enumerator wants to move to the next item in the List, returning True if it does this successfully, otherwise returning False. In this case, it:
- first checks to see if we’re already at the end of the List, in which case it bails out returning False, we’re at the end of the list.
- otherwise, it keeps incrementing the position until either we’re passed the last item or we hit an item that passes our filter criteria.
ShouldIncludeItem invokes our FIlterMethod if one is defined, passing in the current item in the list. It looks like:
function TFilteredList.TFilteredEnumerator.ShouldIncludeItem: Boolean;
begin
Result := True;
if Assigned(FFilterFunction) then
Result := FFilterFunction(FList[FIndex]);
end;
The following code creates a TList<TPerson> and loads it up, then enumerates over each TPerson in the list that is older than 18. You can see the call to SetFilter where we specify the anonymous method that will do the actual filtering. It then clears the filter and enumerates over all the TPerson objects.
PersonList := TFilteredList.Create;
try
PersonList.Add(TPerson.Create('Fred Adult', 37));
PersonList.Add(TPerson.Create('Julie Child', 15));
PersonList.Add(TPerson.Create('Mary Adult', 18));
PersonList.Add(TPerson.Create('James Child', 12));
writeln('--------------- Filtered Person List --------------- ');
PersonList.SetFilter(function(Item : TPerson): boolean
begin
Result := Item.Age >= 18;
end);
for P in PersonList do
begin
writeln(P.ToString);
end;
writeln('--------------- Unfiltered Person List --------------- ');
PersonList.ClearFilter;
for P in PersonList do
begin
writeln(P.ToString);
end;
finally
PersonList.Free;
end;
This produces the following output:
————— Filtered Person List —————
Name = Fred Adult, Age = 37
Name = Mary Adult, Age = 18
————— Unfiltered Person List ————-
Name = Fred Adult, Age = 37
Name = Julie Child, Age = 15
Name = Mary Adult, Age = 18
Name = James Child, Age = 12
The generics come into play so that I can use the same list for something other than TPerson objects, in the example below, Integers:
IntegerList := TFilteredList.Create;
try
IntegerList.Add(1);
IntegerList.Add(2);
IntegerList.Add(3);
IntegerList.Add(4);
writeln('--------------- Filtered Integer List --------------- ');
IntegerList.SetFilter(function(Item : Integer): boolean
begin
Result := Item >= 3;
end);
for I in IntegerList do
begin
writeln(IntToStr(I));
end;
writeln('--------------- Unfiltered Integer List --------------- ');
IntegerList.ClearFilter;
for I in IntegerList do
begin
writeln(IntToStr(I));
end;
finally
IntegerList.Free;
end;
Now, I’m not sure that I really want to write my code like this. I like the fact that there’s a nice separation between the code that decides which items to act on, and the code that actually acts on them, rather than having them all jumbled together inside the for..in loop.
Ironically, that’s also the bit that I don’t like, as I can see that the filter code could possibly be overlooked by someone not familiar with anonymous methods, and think that we were acting on all the items in the list.
Yet again, I want to have my cake and eat it to.
However, as I said at the start this was an experiment to teach me a bit more about anonymous methods, so from that point of view it worked, and hopefully you got something out of it as well.
You can download the code from my delphi-experiments repository on github.