Delphi 2006 introduced enumerators – a way to iterate any kind of collection with the for-in loop. And the VCL is chock-full of enumerators: TStrings has one, so do TComponent, TWinControl, TList, TObjectList and the generic TList<T> and TObjectList<T>.
So I recently set about writing a grid control (the main reason for the long delay since my last post). It’s not the first time that I did this and my previous attempt is a nice-looking and quite capable DB-aware grid that replaces TDBGrid.
This time round, I thought it would be cool to take any old data source, not just TDataSource to feed my grid. I wanted to be able to take both TDataSource objects and also any object that will work with the for-in construct.
This has many complications but I thought I’d show three strategies that I came up with to iterate any enumerator and which one I think is better.
If you are not familiar with how enumerators work, here is a quick description from the official documentation:
“To use the for–in loop construct on a class or interface, the class or interface must implement a prescribed collection pattern. A type that implements the collection pattern must have the following attributes:
- The class or interface 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.”
In my simple example, I am going to fill in a TStrings (a TListBox’s Items property to be exact) with text which I get from my enumerator. To simplify matters a little, I’m going to use Delphi’s new ToString property. And I am going to skimp a little on error checking.
The list that I’d like to iterate is this:
type
TBeatle =class
private
FName:string
;public
function
ToString:string
;override
;constructor
Create(const
Name:string
);end
; TBeatles =class
(TObjectList<TBeatle>);
with this trivial implementation:
{ TBeatle }
constructor
TBeatle.Create(const
Name:string
);begin
FName := Name;end
;function
TBeatle.ToString:string
;begin
Result := FName;end
;
and the list is populated with the obvious choices:
Beatles := TBeatles.Create(True); Beatles.Add(TBeatle.Create('John'
)); Beatles.Add(TBeatle.Create('Paul'
)); Beatles.Add(TBeatle.Create('George'
)); Beatles.Add(TBeatle.Create('Ringo'
));
Technique 1: Events
Let us first consider a Delphi 2006-compatible way to step through the list and add its content to a TStrings.
The first thing you’ll need is a pair of event types – one to read the Current property and one to advance the list with MoveNext:
TCallGetCurrentEvent =procedure
(Enumerator: TObject;var
Value:string
)of
object
; TCallMoveNextEvent =procedure
(Enumerator: TObject;var
Result: Boolean)of
object
;
Actually, both of these should probably be function of object, but I always end up with constructs where the compiler can’t quite determine whether I’m calling the function or taking its address.
Also, here is the declaration of my class method that populates a TStrings with data from an arbitrary enumerator. And yes, I am aware that TStringsFiller is a really, really stupid name.
TStringsFiller =class
public
class
procedure
Fill(Strings: TStrings; Enumerator: TObject; GetCurrent: TCallGetCurrentEvent; MoveNext: TCallMoveNextEvent);end
;
This method is implemented in exactly the way you’d expect:
class
procedure
TStringsFiller.Fill(Strings: TStrings; Enumerator: TObject; GetCurrent: TCallGetCurrentEvent; MoveNext: TCallMoveNextEvent);var
MoveResult: Boolean; Current:string
;begin
MoveNext(Enumerator, MoveResult);while
MoveResultdo
begin
GetCurrent(Enumerator, Current); Strings.Add(Current); MoveNext(Enumerator, MoveResult);end
;end
;
Usage is pretty simple too:
procedure
TForm1.HandleGetCurrent(Enumerator: TObject;var
Value:string
);begin
Value := (Enumeratoras
TList<TBeatle>.TEnumerator) .Current.ToString;end
;procedure
TForm1.HandleMoveNext(Enumerator: TObject;var
Result: Boolean);begin
Result := (Enumeratoras
TList<TBeatle>.TEnumerator) .MoveNext;end
;procedure
TForm1.OldStyleButtonClick(Sender: TObject);begin
TStringsFiller.Fill(ListBox.Items, Beatles.GetEnumerator, HandleGetCurrent, HandleMoveNext);end
;
Technique 2: Anonymous Methods
The technique above is quite simple and is very familiar to almost any Delphi programmer. It could be somewhat cumbersome to use though. Needing to add those event handlers and passing them through is the kind of boilerplate code that we all dislike.
So next up is a variation on the above technique, but one that requires Delphi 2009. Instead of the two event types, we have anonymous method types:
TCallGetCurrent = TFunc<TObject, string
>;
TCallMoveNext = TFunc<TObject, Boolean>;
For some reason, I don’t get the same compiler confusion when using anonymous functions as I do when using events, so the declarations and the Fill method all become a little simpler:
class
procedure
TStringsFiller.Fill(Strings: TStrings; Enumerator: TObject; GetCurrent: TCallGetCurrent; MoveNext: TCallMoveNext);begin
while
MoveNext(Enumerator)do
Strings.Add(GetCurrent(Enumerator));end
;
Also, using it is – I think – a bit more intuitive and natural. I guess it is still a little boilerplate, but it just feels a tad less cumbersome to me.
TStringsFiller.Fill(ListBox.Items, Beatles.GetEnumerator,function
(Enumerator: TObject):string
begin
Result := (Enumeratoras
TList<TBeatle>.TEnumerator) .Current.ToString;end
,function
(Enumerator: TObject): Booleanbegin
Result := (Enumeratoras
TList<TBeatle>.TEnumerator) .MoveNext;end
);
I could really live with either of these two solutions, but what I wanted was a way to just assign any old list to my Source property and let the property setter code figure it out.
If you’re already guessing what solution I went for, good for you. If not, look out for part 2 in a few days’ time. And here’s a clue:
Perfect timing.