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.