Monday, March 9, 2009

Filtering generic collections with anonymous methods

I have recently been adding generics to the ti Object Persistence Framework. As part of that I was asked to add enumerator filtering. I did this using a similar technique to that shown here by Malcolm Groves.

I ended up with code used like this:

for item in Flist.FilteredEnumerator(function (TestObject: TtiOPFTestIntegerProp): Boolean
begin
result:= TestObject.IntField mod 2 = 1;
end) do
begin
inc(intCount);
intSum:= intSum + item.IntField;
end;


I was happy enough with the result, but I wasn't happy with the implementation. I wanted something that was, well, more generic. What I ended up doing was wrapping the existing enumerator into one containing a filter. This will work with any generic collection that descends from TEnumerable. I.e. TList, TQueue, TStack, TDictionary and the TObjectXXX variations

To implement an enumerator, a class must have a GetEnumerator function. This returns an object (or a record) that has the Current property and the MoveNext function. Delphi does a fair amount of work behind the scenes to wrap this all up nicely. See The Delphi Geek's series on enumerators here for more background.

Wrapping an existing enumerator meant I could use the existing GetCurrent and MoveNext for accessing the collection. Filtering then becomes as simple as:

function TFilteredEnumerator<T>.MoveNext: Boolean;
begin
while FEnumerator.MoveNext do
begin
if FPredicate(FEnumerator.Current) then
exit(true);
end;
result:= false;
end;
The full code is as follows:
unit FilteredEnumeratorU;

interface

uses Sysutils, generics.collections;

type
TFilteredEnumerator = class
private
FEnumerator: TEnumerator;
FPredicate: TPredicate;
protected
function DoGetCurrent: T;
public
constructor Create(AEnumerable: TEnumerable; APredicate: TPredicate);
destructor Destroy;
function GetEnumerator: TFilteredEnumerator;
property Current: T read DoGetCurrent;
function MoveNext: Boolean;
end;

implementation

{ TFilteredEnumerator }

constructor TFilteredEnumerator.Create(AEnumerable: TEnumerable; APredicate: TPredicate);
begin
inherited create;
FEnumerator:= AEnumerable.GetEnumerator;
FPredicate:= APredicate;
end;

destructor TFilteredEnumerator.Destroy;
begin
FEnumerator.Free;
inherited Destroy;
end;

function TFilteredEnumerator.DoGetCurrent: T;
begin
result:= FEnumerator.Current;
end;

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

function TFilteredEnumerator.MoveNext: Boolean;
begin
while FEnumerator.MoveNext do
begin
if FPredicate(FEnumerator.Current) then
exit(true);
end;
result:= false;
end;

end.

To use filtering in action, simply do something like:


for xxx in TFilteredEnumerator<T>.Create(queue, function (Arg1: T): Boolean
begin
result:= ...;
end)
do
...


eg


var
queue: TQueue<string>;
cur, combined: string;
filter: TFilteredEnumerator<TTestObject>;
begin
queue:= TQueue<string>.Create;
try
...

for cur in TFilteredEnumerator<string>.Create(queue, function (Arg1: string): Boolean
begin
result:= Arg1 < 'A';
end)
do
begin
combined:= combined + cur;
end;



If you are deriving from a collection, you could also wrap this into a method:


function TFilterableList.Filter(APredicate: TPredicate<ttestobject>): TFilteredEnumerator<ttestobject>;
begin
result:= TFilteredEnumerator<ttestobject>.Create(self, APredicate);
end;
Source can be downloaded from here.

1 comment:

Anonymous said...

That is some truely ugly code heading deep into unmaintainable territory.

Can you imagine coming back in 18 months or more and trying to figure out what it does? (or someone else for that matter)