Monday, March 05, 2007

Fun with enumerators, part 1

Iteration over containers (the 'for ... in' statement) is one of the nice things we Win32 developers got with the BDS 2005. I started using it reluctantly but recently I found out that I'm using and misusing it more and more. I started writing a short article on how to implement multiple enumerators in a class, but somehow it turned into a monster. At the moment I'm up to five parts but actually there may be more. You'll just have to wait and see.

If you don't know anything about for...in statement yet and you're running BDS 2005 or later, open up the help and locate Declarations and Statements topic. Inside the lengthy text (near the end), there's an Iteration Over Containers Using For statements subtopic that explains this theme. BDS 2006 users may simply click this link to open the subtopic in question.

This is not a tutorial on iteration, but still a short demo wouldn't hurt. Following simple program adds three items to a string list and then uses for...in construct to write those three lines out. Then it copies first line to a string and uses another for...in construct to iterate over all characters in the string. Output is shown just below the code.

program ForIteratorDemo;

{$APPTYPE CONSOLE}

uses
SysUtils,
Classes;

var
sl: TStringList;
s : string;
c : char;

begin
sl := TStringList.Create;
sl.Add('Line 1');
sl.Add('Line 2');
sl.Add('Line 3');
for s in sl do
Writeln(s);
s := sl[0];
for c in s do
Write(c, '.');
Writeln;
sl.Free;
Readln;
end.


 



How are enumerators made?



To iterate over something, the target must know how to create an enumerator. Enumerators are the mechanism used by the compiler to implement iterator support for any class, record or interface (in addition to some system types as sets and strings, where iterator support is built into the compiler).



Creating an enumerator is simple.The following instructions are copied directly from Delphi help:





To use the for-in loop construct on a class, the class must implement a prescribed collection pattern. A type that implements the collection pattern must have the following attributes:





  • The class must contain a public instance method called GetEnumerator(). The GetEnumerator() method must return a class, interface, or record type.

  • The class, interface, or record returned by GetEnumerator() must contain a public instance method called MoveNext(). The MoveNext() method must return a Boolean.

  • The class, interface, or record returned by GetEnumerator() must contain a public instance, read-only property called Current. The type of the Current property must be the type contained in the collection.


If the enumerator type returned by GetEnumerator() implements the IDisposable interface, the compiler will call the Dispose method of the type when the loop terminates.



For a practical demonstration, you can open Delphi's Classes.pas or, if you don't have Delphi sources or if you are still runnning pre-2005 Delphi, my own GpLists, which implements two enumerators. The one supporting for...in construct on the TGpIntegerList class is shown below.

type
TGpIntegerList = class;

TGpIntegerListEnumerator = class
private
ileIndex: integer;
ileList : TGpIntegerList;
public
constructor Create(aList: TGpIntegerList);
function GetCurrent: integer;
function MoveNext: boolean;
property Current: integer read GetCurrent;
end;

TGpIntegerList = class
//...
public
function GetEnumerator: TGpIntegerListEnumerator;
//...
end;

{ TGpIntegerListEnumerator }

constructor TGpIntegerListEnumerator.Create(aList: TGpIntegerList);
begin
inherited Create;
ileIndex := -1;
ileList := aList;
end;

function TGpIntegerListEnumerator.GetCurrent: integer;
begin
Result := ileList[ileIndex];
end;

function TGpIntegerListEnumerator.MoveNext: boolean;
begin
Result := ileIndex < (ileList.Count - 1);
if Result then
Inc(ileIndex);
end;

{ TGpIntegerList }

function TGpIntegerList.GetEnumerator: TGpIntegerListEnumerator;
begin
Result := TGpIntegerListEnumerator.Create(Self);
end;


The code is quite simple. TGpIntegerList implements GetEnumerator method, which creates an instance of the TGpIntegerListEnumerator class. That one in turn implements GetCurrent and MoveNext functions and Current property.



The trick when writing an enumerator is to keep in mind that MoveNext is called before the GetCurrent. You can assume that compiler-generated iteration loop looks very similar to this pseudocode:

enumerator := list.GetEnumerator;
while enumerator.MoveNext do
do something with enumerator.Current;
enumerator.Free;


Enough for today. You probably didn't learn anything new but that may change tomorrow when I'll work on a much more interesting task - how to add a second enumerator to a class that already implements one.



 



Technorati tags: , ,

3 comments:

  1. Anonymous10:28

    Very inspiring hint. For the fun I made a TDataSetForIn wich allow to write for any TDataSet:

    procedure TMyForm.Test;
    var
    DS: TDataSetForIn;
    begin
    for DS in TDataSetForIn( MyQuery) do
    Memo1.Lines.Add( DS.FieldByName('ID').AsString);
    end;

    instead of the traditional:
    with MyQuery do begin
    First;
    while not EOF do begin
    Memo1.Lines.Add( DS.FieldByName('ID').AsString);
    Next;
    end;
    end;

    Would be nice that CodeGear could add directly GetEnumerator in TDataSet.

    Code is below if someone interested.

    Renaud.

    type
    TDataSetForIn = class;

    TDataSetEnumerator = class
    private
    FDataSet: TDataSetForIn;
    FDoFirst: boolean;
    public
    constructor Create( ADataSet: TDataSetForIn);
    function GetCurrent: TDataSetForIn;
    function MoveNext: boolean;
    property Current: TDataSetForIn read GetCurrent;
    end;

    TDataSetForIn = class ( TDataSet)
    public
    function GetEnumerator: TDataSetEnumerator;
    end;

    implementation

    { TDataSetEnumerator }

    constructor TDataSetEnumerator.Create(ADataSet: TDataSetForIn);
    begin
    FDoFirst := True;
    FDataSet := ADataSet;
    FDataSet.Open;
    end;

    function TDataSetEnumerator.GetCurrent: TDataSetForIn;
    begin
    Result := FDataSet;
    end;

    function TDataSetEnumerator.MoveNext: boolean;
    begin
    if FDoFirst then begin
    FDoFirst := False;
    FDataSet.First;
    end
    else FDataSet.Next;
    Result := not FDataSet.EOF;
    end;

    { TDataSetForIn }

    function TDataSetForIn.GetEnumerator: TDataSetEnumerator;
    begin
    Result := TDataSetEnumerator.Create( Self);
    end;

    ReplyDelete
  2. Actually, you don't have to write TDataSetForIn class at all (but I'll only cover that in parts 4 and 5).

    ReplyDelete
  3. Anonymous11:48

    Well, I'd like to see that. So I wait for your next posts.

    Thanks,
    Renaud.

    ReplyDelete