Fun with enumerators, part 4 - external 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;
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
[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:
First we need an interface defining GetEnumerator, enumerator factory class implementing this interface and enumerator function.
IStringsEnumReversedFactory = interface
Then we need an enumerator, but that's trivial especially as we've written many of them already.
TStringsEnumReversed = class
We can now write a simple tester:
procedure TfrmFunWithEnumerators.btnReverseClick(Sender: TObject);
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.
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);
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.