Fun with enumerators, part 2 - multiple enumerators
In the first installment of this series, we just repeated some well-known facts about the for..in language construct and the implementation of enumerators. Today I'll show a way to implement additional enumerators for a class.
To start, let's write some test framework. Instead of creating a new class implementing an enumerator, we'll just inherit from the TStringList. That way, we get existing TStringList enumerator 'for free'.
TCustomStringList = class(TStringList)
To make things simple, an instance of this class is initialized with one-letter strings from 'a' to 'z' in form's OnCreate handler.
procedure TfrmFunWithEnumerators.FormCreate(Sender: TObject);
To test it, we'll iterate over the list and collect all lines into a single line.
procedure TfrmFunWithEnumerators.btnStandardIteratorClick(Sender: TObject);
And here's a result of this test code:
All good, all well, all very trivial. But our mission was not to test existing enumerator, but to add a new one. So let's take a look at the pseudo-code describing the inner working of the for..in implementation.
enumerator := list.GetEnumerator;
The important fact is that enumerator is created directly from the parameter that is passed to the for..in construct (list, in our case). After that, this parameter is not needed anymore and for..in only operates on the enumerator.
To get a new behaviour, we have to provide a different enumerator. To do that, we have to pass a different object to the for..in loop. This object has to provide a GetEnumerator function, which the compiler will use to implement for..in.
In short, we need a factory class that will generate new enumerator and a property inside our main class that will provide this factory to the for..in loop.
TCSLEnumEverySecondFactory = class
Now we can write
procedure TfrmFunWithEnumerators.btnSkipEverySecondClick(Sender: TObject);
and Delphi compiler will translate it (approximately) into
enumerator := FslTest.SkipEverySecond.GetEnumerator;
We have a winner! That's exactly what we wanted to achieve.
The other parts of the puzzle are simple. The SkipEverySecond factory can be created in the TCustomStringList constructor. We have to pass it Self object (for the reason we'll come to in a minute).
The factory class is simple. It has to remember the owner (TCustomStringList) that was passed to the constructor so that it can pass it on to the enumerator whenever compiler requests one.
constructor TCSLEnumEverySecondFactory.Create(owner: TCustomStringList);
The enumerator is just a variation on a standard string list enumerator - it increments list index by 2 instead of 1 and skips every second item in the list. Yes, there is no rule saying that enumerator must return all entries in the enumerated object. In fact, you can be very creative inside the enumerator. We'll return to it some other day.
constructor TCSLEnumEverySecond.Create(owner: TCustomStringList);
And the results are ...
That wasn't so hard, wasn't it? Tomorrow we'll look into an even niftier topic - enumerators with parameters.