Malcolm has been writing code for nearly 30 years, on everything from IBM mainframes to embedded devices. Someday he hopes to get really good at it.


Email Newsletter

Anonymous Methods, Generics and Enumerators

Photo by boskizzi (Used under Creative Commons) 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<t> = reference to function(Item : T) : boolean;
  TFilteredList<t> = class(TList<t>)
  private
    FFilterFunction: TFilterFunction<t>;
  public
  type
      TFilteredEnumerator = class(TEnumerator<t>)
      private
        FList: TFilteredList<t>;
        FIndex: Integer;
        FFilterFunction : TFilterFunction<t>;
        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<t>;
                           AFilterFunction : TFilterFunction<t>);
        property Current: T read GetCurrent;
        function MoveNext: Boolean;
      end;
    function GetEnumerator: TFilteredEnumerator; reintroduce;
    procedure SetFilter(AFilterFunction : TFilterFunction<t>);
    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<t>.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<t>.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<tperson>.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 &gt;= 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<integer>.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 &gt;= 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.

Malcolm Groves

Malcolm has been writing code for nearly 30 years, on everything from IBM mainframes to embedded devices. Someday he hopes to get really good at it.

4 Comments » for Anonymous Methods, Generics and Enumerators
  1. David S says:

    This is really cool to see in Delphi as opposed to C++.

    It illustrates again how much more “economical” the Object Pascal language is vs. C++.

    The example also shows how blurred the line is getting between dealing with data that resides in a database vs. data that resides elsewhere.

    Thanks!
    -David

  2. Alan Clark says:

    Another great article, keep them coming!

    Would it be possible to add an enumerator to TList using a class helper?

  3. Hey Malcolm,

    Have you considered adding a GetEnumerator to the enumerator class? It would just return self. Then you could construct it in the for…in statement and pass the anonymous function to it’s constructor. Then you could do this:

    FilterFunction := function(Item : TPerson): boolean
    begin
    Result := Item.Age >= 18;
    end;

    for P in TFilteredEnumerator.Create(PersonList, FilterFunction) do
    begin
    writeln(P.ToString);
    end;

    I have a more complete example at http://www.winestain.com.au/?e=16

    N@

  4. Malcolm says:

    Hi N@

    I’m going to use that somewhere else actually, thanks. Nice one.

    Cheers
    Malcolm

Leave a Reply