[Part 1 - Introduction, Part 2 - Additional enumerators, Part 3 - Parameterized enumerators.]
Welcome back, dear reader. If you were following this short series since Day 1, you now know how to create additional enumerators for a class. Today we'll do something even more interesting – we'll add an enumerator to a class that we cannot modify or derive from.
For example, in comments to Part 1 Renaud created enumerator for TDataSet by descending from it. His solution, however, is not practical when TDataSet is created somewhere deep in the class hierarchy.
Another example - one that we'll use today - is additional enumerator for TStrings. This class is used in various stock VCL components (ListBox.Items, Memo.Lines ...). It already provides an enumerator (iterating over all strings in the container), but just for the fun of it we'll add a reverse enumerator – one that will start with the last string and proceed toward beginning of the container.
Let's take another look at the pseudocode describing compiler-generated implementation of a generic for..in loop.
enumerator := list.GetEnumerator;
while enumerator.MoveNext do
enumerator.Free;
In Part two we used enumerator := list.SomeProperty.GetEnumerator to access secondary enumerator and in Part three we used enumerator := list.SomeFunction(param).GetEnumerator to access parameterized enumerator. Delphi compiler is not picky when parsing parameters for the for..in loop. We can provide it with anything that implements GetEnumerator function. And nobody says that this 'anything' must come from a same class hierarchy as the enumerated targed. We can simply write a global function that will return some object with public GetEnumerator.
Let's repeat this: "There is no enforced connection between the factory that provides GetEnumerator and the structure we are enumerating." In fact, there may not be any structure at all. Delphi provider will be perfectly satisfied with this code generating Fibonacci numbers as long as we write correct factory and enumerator:
for i in Fibonacci(10) do
Writeln(i);
[I promise to show you the code to Fibonacci enumerator tomorrow. Or maybe the day after.]
By now it should be obvious what I'm leading at. We can write a global function taking TStrings parameter and returning an enumerator factory for this TStrings. We would then use it like this:
for s in ExternalEnumerator(stringList) do
ExternalEnumerator takes object we want to be enumerated, creates factory object for it and returns this factory object. Compiler then calls GetEnumerator on that factory object to get enumerator object. Then it uses enumerator object to enumerate stringList. At the end, compiler destroys enumerator object. But still the factory object exists and is not destroyed. How can we destroy it when it is no longer needed?
Simple, we will use helpful Delphi compiler. Instead of returning object reference from the ExternalEnumerator, we'll return an interface. Compiler will automatically manage its lifetime and will destroy it when it's no longer needed.
It looks like we have all parts ready now:
- We will write interface defining GetEnumerator function and enumerator factory implementing this interface.
- External enumerator function will create new enumerator factory object and return it.
- Compiler will call GetEnumerator on the factory object, complete the for..in loop and destroy the enumerator object.
- Some time later (maybe at the end of the for..in loop, maybe at the end of the method containing for..in loop - this is implementation specific and may differ between different versions of the compiler) enumerator factory interface will go out of scope and compiler will destroy the factory object.
- And that's all folks.
First we need an interface defining GetEnumerator, enumerator factory class implementing this interface and enumerator function.
IStringsEnumReversedFactory = interface
function GetEnumerator: TStringsEnumReversed;
end;
TStringsEnumReversedFactory = class(TInterfacedObject, IStringsEnumReversedFactory)
private
FOwner: TStrings;
public
constructor Create(owner: TStrings);
function GetEnumerator: TStringsEnumReversed;
end;
function EnumReversed(strings: TStrings): IStringsEnumReversedFactory;
begin
Result := TStringsEnumReversedFactory.Create(strings);
end;
Then we need an enumerator, but that's trivial especially as we've written many of them already.
TStringsEnumReversed = class
private
FOwner: TStrings;
FListIndex: integer;
public
constructor Create(owner: TStrings);
function GetCurrent: string;
function MoveNext: boolean;
property Current: string read GetCurrent;
end;
constructor TStringsEnumReversed.Create(owner: TStrings);
begin
FOwner := owner;
FListIndex := owner.Count;
end;
function TStringsEnumReversed.GetCurrent: string;
begin
Result := FOwner[FListIndex];
end;
function TStringsEnumReversed.MoveNext: boolean;
begin
Result := FListIndex > 0;
if Result then
Dec(FListIndex);
end;
We can now write a simple tester:
procedure TfrmFunWithEnumerators.btnReverseClick(Sender: TObject);
var
ln: string;
s : string;
begin
ln := '';
for s in EnumReversed(FslTest) do
ln := ln + s;
lbLog.Items.Add('External enumerator: ' + ln);
end;
And here's a proof that EnumReversed really works.
It is maybe not obvious what really happens here, so let's take another look at this for..in loop - this time written as Delphi compiler implements it.
var
ln: string;
s: string;
enum: TStringsEnumReversed;
tmpIntf: IStringsEnumReversedFactory;
begin
ln := '';
tmpIntf := EnumReverse(FslTest);
enum := tmpIntf.GetEnumerator;
while enum.MoveNext do
ln := ln + enum.Current;
enum.Free;
lbLog.Items.Add('External enumerator: ' + ln);
tmpIntf := nil;
end;
As we've mentioned at the very beginning, this approach allows us to use enumerators on base classes (TStrings in our example), so here's a simple code that reverses items in the TListBox I'm using to display test results:
procedure TfrmFunWithEnumerators.btnReverseLogClick(Sender: TObject);
var
s : string;
sl: TStringList;
begin
sl := TStringList.Create;
for s in EnumReversed(lbLog.Items) do
sl.Add(s);
lbLog.Items.Assign(sl);
sl.Free;
end;
Result:
That's all for today. Tomorrow I'll show you another trick that will allow you to write for..in loop from the last example as for s in lbLog.Items.EnumReversed do.