Monday, April 21, 2008

Enumerating XML nodes

I just submitted an update to OmniXmlUtils.pas - a helper library for the Delphi implementation of the XML DOM model, the OmniXML. It allows you to walk over child nodes with a simple enumerator (but you already saw that coming, huh?).

In OmniXML (and in MS XML on which the OmniXML interface is based), you access child nodes via IXMLNodeList interface. Actually, when you access any subset of nodes (for example, when you call SelectNodes and pass it an XPath expression), you are using IXMLNodeList. More and more I was using it, more I hated its stupid interface.

  IXMLCustomList = interface
function GetLength: Integer;
function GetItem(const Index: Integer): IXMLNode;
property Item[const Index: Integer]: IXMLNode read GetItem;
property Length: Integer read GetLength;
end;

IXMLNodeList = interface(IXMLCustomList)
procedure Reset;
function NextNode: IXMLNode;
end;
[I removed declarations of all functions that are not used when walking over the list.]


Basically, I have two options with the current design. I can use a standard for loop using Length and Item[] or I can use NextNode and while loop.


Or, of course, I can write an enumerator - and that's what I did. XMLEnumNodes takes an IXMLNodeList and wraps it in an enumerator. Even better - it takes an xml node or document and an XPath expression and runs SelectNodes automatically for me so I can write code like this:

for nodePassword in XMLEnumNodes(xmlConfig, '//*/PasswordHash') do
SetTextChild(nodePassword, '(removed)');

Now that's what I call a nice code!


If you can't wait for tomorrow update to the OmniXML daily snapshot, you can download OmniXMLUtils 1.25 here.

Labels: , , ,

Friday, March 21, 2008

Walking the key-value container

The recent discussion in comments to my latest articles (Fun with enumerators - Walking the (integer) list, On bad examples and smelly code) caused a shift in my perspective. I was always treating my TGpIntegerObjectList and TGpInt64ObjectList as lists with some baggage attached, but in practice I'm almost never using them that way. Most of the time I treat them as a background storage for key-value pairs.

What's the difference? Most of the time, I don't care about item indices. When I use my lists as containers, I never need to know where in the list some (Key, Value) pair is stored. OK, almost never. When I'm deleting from the list, I sometimes use the index, just for the performance purposes. And when I access the Value part, I have to find the index with IndexOf and then use it to reference the Objects property. There are probably other cases too - but that's something that you just have to take into account if you want to use a list as a storage media.

From time to time I'm extending lists in my GpLists unit with small wrappers that help me not to use list indices in some specific situation. Today, I used the new Walk enumerator in some code and asked myself: "Why does it have to return list index? Why couldn't it return a (Key, Value) pair?"

Good question. Why couldn't it? It turns out that it could.

A little enumerator that could

Let's set up some assumptions first. Assume that I have this pointless list.

il := TGpIntegerObjectList.Create;
il.AddObject(1, TGpString.Create('one'));
il.AddObject(2, TGpString.Create('two'));
il.AddObject(3, TGpString.Create('three'));
Further assume that I want to walk over it and display both number and text for each item. I can do this with a standard loop.
for idx := 0 to il.Count - 1 do
Log('%d: %s', [il[idx], TGpString(il.Objects[idx]).Value]);
Or with my index-returning walker.
for idx in il.Walk do
Log('%d: %s', [il[idx], TGpString(il.Objects[idx]).Value]);
But what I'd really like to do is.
for kv in il.WalkKV do
Log('%d: %s', [kv.Key, TGpString(kv.Value).Value]);
Or even better.
for kv in il.WalkKV do
Log('%d: %s', [kv.Key, kv.StrValue]);

In other words, I want to return not a single item, but a pair of items from the enumerator. Of course, Delphi expressions can only return a single result and not a tuple so we have to provide a wrapper for enumerated (Key, Value) pairs. We have to pack them in a record, class or interface.


Getting all self-destructive


My first idea was to return an interface to a key-value object from the enumerator.

  IGpKeyValue = interface
function GetKey: int64;
function GetValue: TObject;
property Key: int64 read GetKey;
property Value: TObject read GetValue;
end; { IGpKeyValue }

TGpKeyValue = class(TInterfacedObject, IGpKeyValue)
private
kvKey: int64;
kvValue: TObject;
protected
function GetKey: int64;
function GetValue: TObject;
public
constructor Create(key: int64; value: TObject);
property Key: int64 read GetKey;
property Value: TObject read GetValue;
end; { TGpKeyValue }

TGpIntegerObjectListWalkKVEnumerator = class
//...
function GetCurrent: IGpKeyValue;
property Current: IGpKeyValue read GetCurrent;
end; { TGpIntegerObjectListWalkKVEnumerator }

function TGpIntegerObjectListWalkKVEnumerator.GetCurrent: IGpKeyValue;
var
idx: integer;
begin
idx := wkeListEnumerator.GetCurrent;
Result := TGpKeyValue.Create(wkeListEnumerator.List[idx],
TGpIntegerObjectList(wkeListEnumerator.List).Objects[idx]);
end; { TGpIntegerObjectListWalkKVEnumerator.GetCurrent }

That surely works fine, but guess what - it's incredibly slow. I wouldn't expect anything else - after all an object is allocated for every enumerated value, plus all that complications with interface reference counting...


I did some testing, of course. Thousand iterations over a list with 10.000 elements. Results are quite interesting. 5 milliseconds for a standard for loop. 50 milliseconds for my Walk enumerator. 5 seconds for interface-based WalkKV. Ouch! Let's return to the drawing board...


One allocation is enough


My next idea was to return not an interface, but an object. When you return an object, you actually return a pointer to the object data, which is quite fast. It would not help much if I would recreate this object every time the GetCurrent is called, but luckily there is no real reason to do that. I can create the object when enumerator is created and destroy it when enumerator is destroyed.

  TGpKeyValue = class
private
kvKey : int64;
kvValue: TObject;
public
property Key: int64 read kvKey write kvKey;
property Value: TObject read kvValue write kvValue;
end; { TGpKeyValue }

TGpIntegerObjectListWalkKVEnumerator = class
private
wkeCurrentKV: TGpKeyValue;
wkeListEnumerator: TGpIntegerListWalkEnumerator;
public
constructor Create(aList: TGpIntegerObjectList; idxFrom, idxTo: integer);
destructor Destroy; override;
//...
function GetCurrent: TGpKeyValue;
property Current: TGpKeyValue read GetCurrent;
end; { TGpIntegerObjectListWalkKVEnumerator }

constructor TGpIntegerObjectListWalkKVEnumerator.Create(aList: TGpIntegerObjectList;
idxFrom, idxTo: integer);
begin
inherited Create;
wkeListEnumerator := TGpIntegerListWalkEnumerator.Create(aList, idxFrom, idxTo);
wkeCurrentKV := TGpKeyValue.Create;
end; { TGpIntegerObjectListWalkKVEnumerator.Create }

destructor TGpIntegerObjectListWalkKVEnumerator.Destroy;
begin
FreeAndNil(wkeCurrentKV);
FreeAndNil(wkeListEnumerator);
inherited;
end; { TGpIntegerObjectListWalkKVEnumerator.Destroy }

function TGpIntegerObjectListWalkKVEnumerator.GetCurrent: TGpKeyValue;
var
idx: integer;
begin
idx := wkeListEnumerator.GetCurrent;
wkeCurrentKV.Key := wkeListEnumerator.List[idx];
wkeCurrentKV.Value := TGpIntegerObjectList(wkeListEnumerator.List).Objects[idx];
Result := wkeCurrentKV;
end; { TGpIntegerObjectListWalkKVEnumerator.GetCurrent }

BTW, you can see another trick in this implementation - enumeration by delegation. I'm reusing my Walk enumerator to do the actual walking.


That works much faster than the interface-based version - 300 ms for my test case. It's still six times slower than the Walk enumerator, though, and it is not really obvious why the difference is so big. Leave that be, for a moment.


The third approach would be to use a record to store the current (Key, Value) pair. Then there is no allocation/deallocation at all, but resulting code is not faster. Record-based enumerator needs about 500 ms to run the test case.


This slowdown occurs because record is not returned as a pointer, but as a full copy. In the class-based scenario, GetCurrent returns a pointer to the TGpKeyValue object and that pointer is passed in a register. In the record version, GetCurrent returns not a pointer to the record, but the record itself - it copies full record to the stack so the caller can use this copy - and that is waaaay slower.


The speed difference


Let's return to that major speed difference between Walk and WalkKV. I looked at the code, but couldn't find any good reason. Then I turned to the CPU view and it was evident. The problem lied not in the enumerator, but in my poorly designed benchmarking code :(


You see, I was timing multiple repetitions of these three loops:

for idx := 1 to 10000 do 
;

for idx in il.Walk do
;

for kv in il.WalkKV do
;

Three empty loops, so I'm timing just the enumeration, yes? No!


First loop just runs from 1 to 10000. Trivial job and compiler will generate great code.


Second loop does the same, but with more overhead.


Third loop does much more than that. It also accesses il[] and il.Objects[] (inside the GetCurrent).


In reality, code inside the for statement in the first two cases would have to access il[] and il.Objects[] by itself. The code inside the third for statement has no need for that - it gets data neatly prepared in kv.Key and kv.Value.


I changed the loops to:

for idx := 0 to il.Count - 1 do begin
il[idx];
obj := il.Objects[idx];
end;

for idx in il.Walk do begin
il[idx];
obj := il.Objects[idx];
end;

for kv in il.WalkKV do begin
kv.Key;
obj := kv.Value;
end;

I'm just accessing and throwing the Items[]/Key information away and then I copy the Objects[]/Value information into the local variable. All loops are now running comparable jobs.


Results are now significantly different. Simple for loop needs 300 ms to run the test, Walk version needs 310 ms (really small difference) and WalkKV version needs 470 ms. It is still slower, but by less than the factor of two. If there would be a real code inside the for loop, the difference would be unnoticeable.


Morale? You should benchmark, but you should also check that you're benchmarking the right thing!


Syntactic sugar


The generic version of WalkKV only supports this kind of code:

for kv in il.WalkKV do
Log('%d: %s', [kv.Key, TGpString(kv.Value).Value]);

But there's a really neat trick to change this into

for kv in il.WalkKV do
Log('%d: %s', [kv.Key, kv.StrValue]);

without modifying the WalkKV itself. Can you guess it? Class helpers, of course.


You just have to declare a simple helper in the WalkKV consumer (no need to change the WalkKV itself) and you can use the StrValue instead of TGpString(kv.Value).Value.

  TGpKeyStrValue = class helper for TGpKeyValue
public
function StrValue: string; inline;
end; { TGpKeyStrValue }

function TGpKeyStrValue.StrValue: string;
begin
Result := TGpString(Self.Value).Value;
end;

Conclusion: class helpers are great. Key-value walker is useful. Enumerator-induced performance loss is negligible. Enumerators are fun.


---


I've tagged this and all previous enumerator-related articles with the enumerators label so you can now access them all at once via this simple link.

Labels: , , , , ,

Friday, March 14, 2008

Fun with enumerators - Walking the (integer) list

Do you hate such code?

    idx := 0;
while idx < il.Count do
if ShouldBeRemoved(il[idx]) then
il.Delete(idx)
else
Inc(idx);

If you do, read on!


TGpIntegerList [For the purpose of this blog entry, TGpIntegerList token is redefined to implicitly expands to "TGpIntegerList, TGpInt64List, and all classes derived from them."]  included enumeration support since Delphi 2005 times. You could, for example, write

  il := TGpIntegerList.Create([1, 3, 5, 7]);
try
for i in il do
Log('%d', [i]);
finally FreeAndNil(il); end;

and get numbers 1, 3, 5, and 7 in the log.


Slicing ...


Recently, I've extended TGpIntegerList with two additional enumerators, both (of course) springing from practice. I've noticed that I still write lots of 'old school' for loops because I want to do something special with the first element. For example, take this simple code fragment that finds a maximum element in the list.

    max := il[0];
for idx := 1 to il.Count - 1 do
if il[idx] > max then
max := il[idx];

To do such thing with for-each, you have to introduce a boolean flag.

    gotMax := false;
for i in il do
if not gotMax then begin
max := i;
gotMax := true;
end
else if i > max then
max := i;

Ugly.


Much nicer implementation can be written with the new Slice() enumerator.

    max := il[0];
for i in il.Slice(1) do
if i > max then
max := i;

Slice takes two parameters representing starting and ending index in the list and returns all values between them (indices are inclusive). Ending index can be omitted in which case it will default to Count-1 (i.e. to the end of the list).


Slice enumerator is implemented in a traditional manner - with the enumerator factory. For more details see my older articles and GpLists source code.


... And Walking


The second and much more interesting enumerator solves the problem shown in the introduction to this article. I noticed that I write lots of code that iterates over some list and removes some entries while keeping others intact. Typical scenario: removing timed-out clients from the server's list.


Something like that is impossible to do with the TGpIntegerList default enumerator. Firstly because this enumerator returns list elements and not list indices (and we typically have to access the object stored in the .Objects[] part of the list during the process) and secondly because the enumerator does not work correctly if enumerated list is modified behind its back. The second issue also prevents the use of standard for loop.


To solve both problems at once, I've introduced the Walk enumerator. It returns list indices instead of list elements and functions correctly if Add, Insert or Delete are used on the list while enumerator is active. (The second part is implemented by using TGpIntegerList internal notification mechanism, introduced just for this purpose.)


Now I can write:

    for idx in il.Walk do
if ShouldBeRemoved(il[idx]) then
il.Delete(idx);

Implementation? Check the source, Luke. It's not complicated.


Now if only I could add such enumerator to the TObjectList ...

Labels: , , , ,

Wednesday, February 06, 2008

TWinControl.Controls Enumerator, Revisited

Fredrik Loftheim made an interesting observation to my previous article on TWinControl.Controls enumerator - namely that the enumerator factory doesn't have to be interface based. It can be replaced by a record. Source is almost the same as before, but we can skip the interface declaration. Generated code is simpler as the enumerator factory is simply allocated from the stack and automatically destroyed when it goes out of scope. No interface support and no reference counting is required. Simpler and faster, that's the way to go.

interface

type
TControlEnumerator = record
strict private
ceClass : TClass;
ceIndex : integer;
ceParent: TWinControl;
public
constructor Create(parent: TWinControl; matchClass: TClass);
function GetCurrent: TControl;
function MoveNext: boolean;
property Current: TControl read GetCurrent;
end; { TControlEnumerator }

TControlEnumeratorFactory = record
strict private
cefClass : TClass;
cefParent: TWinControl;
public
constructor Create(parent: TWinControl; matchClass: TClass);
function GetEnumerator: TControlEnumerator;
end; { TControlEnumeratorFactory }

function EnumControls(parent: TWinControl; matchClass: TClass = nil): TControlEnumeratorFactory;

implementation

function EnumControls(parent: TWinControl; matchClass: TClass = nil): TControlEnumeratorFactory;
begin
Result := TControlEnumeratorFactory.Create(parent, matchClass);
end; { EnumControls }

{ TControlEnumerator }

constructor TControlEnumerator.Create(parent: TWinControl; matchClass: TClass);
begin
ceParent := parent;
ceClass := matchClass;
ceIndex := -1;
end; { TControlEnumerator.Create }

function TControlEnumerator.GetCurrent: TControl;
begin
Result := ceParent.Controls[ceIndex];
end; { TControlEnumerator.GetCurrent }

function TControlEnumerator.MoveNext: boolean;
begin
Result := false;
while ceIndex < (ceParent.ControlCount - 1) do begin
Inc(ceIndex);
if (ceClass = nil) or (ceParent.Controls[ceIndex].InheritsFrom(ceClass)) then begin
Result := true;
break; //while
end;
end; //while
end; { TControlEnumerator.MoveNext }

{ TControlEnumeratorFactory }

constructor TControlEnumeratorFactory.Create(parent: TWinControl; matchClass: TClass);
begin
cefParent := parent;
cefClass := matchClass;
end; { TControlEnumeratorFactory.Create }

function TControlEnumeratorFactory.GetEnumerator: TControlEnumerator;
begin
Result := TControlEnumerator.Create(cefParent, cefClass);
end; { TControlEnumeratorFactory.GetEnumerator }

The new version of the TWinControl.Controls enumerator plus some other stuff is available at http://gp.17slon.com/gp/files/gpvcl.zip.

Labels: , , , ,

Friday, February 01, 2008

TWinControl.Controls Enumerator

I'm still having fun with enumerators. The latest I wrote is a filtering enumerator for TWinControl.Controls[] that allows me to write code like this:

var
control: TControl;

for control in EnumControls(pnlAttributes, TSpeedButton) do
TSpeedButton(control).Enabled := false;

I found it interesting that Borland built a Components[] enumerator in the VCL, but not a Controls[] enumerator.


The EnumControls interface is simple. It takes a starting point for enumeration and an optional class filter. By specifying the latter, you tell the enumerator that you're only interested in child controls of a specified class.

function EnumControls(parent: TWinControl; 
matchClass: TClass = nil): IControlEnumeratorFactory;

As it is used in a for..in loop, EnumControl must return a class or interface that implements GetEnumerator function and that is exactly what it does. GetEnumerator, on the other hand, creates an instance of the TControlEnumerator class, which implements the actual enumeration.

type
TControlEnumerator = class
strict private
ceClass : TClass;
ceIndex : integer;
ceParent: TWinControl;
public
constructor Create(parent: TWinControl; matchClass: TClass);
function GetCurrent: TControl;
function MoveNext: boolean;
property Current: TControl read GetCurrent;
end; { TControlEnumerator }

IControlEnumeratorFactory = interface
function GetEnumerator: TControlEnumerator;
end; { IControlEnumeratorFactory }

On the implementation side, EnumControls creates an instance of the TControlEnumeratorFactory class, which implements the IControlEnumeratorFactory interface.

function EnumControls(parent: TWinControl; matchClass: TClass = nil): IControlEnumeratorFactory;
begin
Result := TControlEnumeratorFactory.Create(parent, matchClass);
end; { EnumControls }

type
TControlEnumeratorFactory = class(TInterfacedObject, IControlEnumeratorFactory)
strict private
cefClass: TClass;
cefParent: TWinControl;
public
constructor Create(parent: TWinControl; matchClass: TClass);
function GetEnumerator: TControlEnumerator;
end; { GetEnumerator }

TControlEnumeratorFactory just stores parent and matchClass parameters for later use in the GetEnumerator function.

constructor TControlEnumeratorFactory.Create(parent: TWinControl; matchClass: TClass);
begin
cefParent := parent;
cefClass := matchClass;
end; { TControlEnumeratorFactory.Create }

function TControlEnumeratorFactory.GetEnumerator: TControlEnumerator;
begin
Result := TControlEnumerator.Create(cefParent, cefClass);
end; { TControlEnumeratorFactory.GetEnumerator }

GetEnumerator creates an instance of the TControlEnumerator class, which implements the actual enumeration in a pretty standard manner. Only the MoveNext method is slightly more complicated than usual because it must optionally check the matchClass parameter.

constructor TControlEnumerator.Create(parent: TWinControl; matchClass: TClass);
begin
inherited Create;
ceParent := parent;
ceClass := matchClass;
ceIndex := -1;
end; { TControlEnumerator.Create }

function TControlEnumerator.GetCurrent: TControl;
begin
Result := ceParent.Controls[ceIndex];
end; { TControlEnumerator.GetCurrent }

function TControlEnumerator.MoveNext: boolean;
begin
Result := false;
while ceIndex < (ceParent.ControlCount - 1) do begin
Inc(ceIndex);
if (ceClass = nil) or (ceParent.Controls[ceIndex].InheritsFrom(ceClass)) then begin
Result := true;
break; //while
end;
end; //while
end; { TControlEnumerator.MoveNext }

It's instructive to see how all those parts play together when the code fragment from the beginning of this blog entry is executed.

for control in EnumControls(pnlAttributes, TSpeedButton) do
TSpeedButton(control).Enabled := false;


  • EnumControls is called. It creates an instance of the IControlEnumeratorFactory. TControlEnumeratorFactory constructor stores parent and matchClass parameters into internal fields.

    • TControlEnumeratorFactory.GetEnumerator is called. It creates an instance of the TControlEnumerator and passes it internal copies of parent and matchClass parameters.

      • TControlEnumerator's MoveNext and Current are used to enumerate over the parent's child controls.

    • For..in loop terminates and compiler automatically destroys the object, created by the GetEnumerator method.

  • Sometime later, the method containing this for..in loop terminates and compiler automatically frees the IControlEnumeratorFactory instance created in first step.

The TWinControl.Controls enumerator plus some other stuff is available at http://gp.17slon.com/gp/files/gpvcl.zip.

Labels: , , , ,

Saturday, January 12, 2008

TDataset Enumerator

You already know that I love all things enumeratorish. Just recently I found a great example that combines three kinds of Delphi magic to allow the programmer to write very simple TDataset enumeration loops. [And for the sake of my life I can't remember where I first saw the link to this code. Apologies to the original author.] [Updated 2008-01-14: Jim McKeeth found my source. Apparently Nick Hodges was the first to blog about this enumerator. Thanks, Nick!]

This TDataset enumerator was written by Uwe Raabe and is freely available in the CodeGear's Code Central as item #25386.

The three trick he uses are:

  1. Data helper is used to push enumerator support into TDataset.
  2. Standard enumerator support to enumerate over dataset and return special descendant of the Variant type for each record.
  3. Custom variant type to automagically access record fields by by adding .RecordFieldName to the variant variable. [I must admit I didn't even know this was possible to do in Delphi.]

All together those tricks allow you to write very simple TDataset iteration loops (code taken from the example in the DN #25386):

var
Employee: Variant;
S: string;

for Employee in QuEmployee do begin
S := Trim(Format('%s %s', [Employee.First_Name, Employee.Last_Name]));
if Employee.Hire_Date < EncodeDate(1991, 1, 1) then
S := '*' + S;
end;

Excellent job!

Labels: ,

Sunday, March 11, 2007

Fun with enumerators

Boy, was this an interesting trip.

For the last six days I was writing a series of articles on Delphi enumerators, one day each. In some way, this was very similar to something else I like to do - writing magazine articles on computer-related topics. So similar that I planned this series exactly as I'm planning an article. In some other way, it was also very different. Later posts I adapted based on feedback from earlier ones. For example, Part 6 was not included in the original article outline. This topic came to my mind while I was reading reader comments. In a way, it was like working with a very eager editor who is checking every chapter immediately I'm finished with it. Or, if you want, it was similar to pair programming.

In a way, writing this series was more like writing a book. If that's so, I have something more to write - a table of contents. It will help new readers to read whole series or just find the part they are interested in. So without further ado, here is the

Table of Contents

Part 1 - Introduction

Contains a short introduction on Delph iterators (for..in statement) and describes Delphi support for iterator extensibility.

Part 2 - Additional enumerators

Shows how to add an additional enumerator to a class that already contains one.

Part 3 - Parameterized enumerators

This chapter takes Part 2 topic one level further by introducing enumerator parameters.

Part 4 - External enumerators

In this chapter you'll learn how to create enumerators without changing the class they are enumerating.

Part 5 - Class helper enumerators

Shows how to create additional enumerators using class helpers and how to use same technique to add enumerators to classes that don't have one.

Part 6 - Generators

The last chapter shows how to write enumerators that work on their own data, not on some external structure. It also includes full source code of a demo program and all enumerators described in the series.

Make sure you'll also read comments to those posts - some quite interesting ideas are hidden there.

[If you liked my articles, feel free to tell others about them. Thanks!]

Updated 2007-11-13

Hallvard Vassbotn posted interesting observations on enumerator performance in More fun with Enumerators.

Allen Bauer found a new use of the enumerator pattern in Stupid Enumerator Tricks - And now for something completely different.

Technorati tags: , ,

Labels: ,

Saturday, March 10, 2007

Fun with enumerators, part 6 - generators

[Part 1 - Introduction, Part 2 - Additional enumerators, Part 3 - Parameterized enumerators, Part 4 - External enumerators, Part 5 - Class helper enumerators.]

This was a long and interesting trip but it has to end some day. And it will end today. Just one last small topic to cover ...

in Part 4 I mentioned that we could abuse enumerators to the point where they are not working on any external structure. My example for such generator which is providing its own enumeration data was

for i in Fibonacci(10) do
Writeln(i);


and I promised to show you how to write it.



Again, we will start with a 'standard' enumerator factory, the one that we used a lot in parts 4 and 5.

  IEnumFibonacciFactory = interface
function GetEnumerator: TEnumFibonacci;
end;

TEnumFibonacciFactory = class(TInterfacedObject, IEnumFibonacciFactory)
private
FUpperBound: integer;
public
constructor Create(upperBound: integer);
function GetEnumerator: TEnumFibonacci;
end;

function Fibonacci(upperBound: integer): IEnumFibonacciFactory;


Enumerator is slightly trickier this time - it prepares requested data in the constructor and uses MoveNext to move over this data.

  TEnumFibonacci = class
private
FFibArray: array of integer;
FIndex: integer;
public
constructor Create(upperBound: integer);
function GetCurrent: integer;
function MoveNext: boolean;
property Current: integer read GetCurrent;
end;

constructor TEnumFibonacci.Create(upperBound: integer);
var
i: integer;
begin
SetLength(FFibArray, upperBound);
if upperBound >= 1 then
FFibArray[0] := 1;
if upperBound >= 2 then
FFibArray[1] := 1;
for i := 2 to upperBound - 1 do
FFibArray[i] := FFibArray[i-1] + FFibArray[i-2];
FIndex := -1;
end;

function TEnumFibonacci.GetCurrent: integer;
begin
Result := FFibArray[FIndex];
end;

function TEnumFibonacci.MoveNext: boolean;
begin
Result := FIndex < High(FFibArray);
if Result then
Inc(FIndex);
end;


Test code follows the well-learned pattern.

procedure TfrmFunWithEnumerators.btnFibonacciClick(Sender: TObject);
var
i : integer;
ln: string;
begin
ln := '';
for i in Fibonacci(10) do begin
if ln <> '' then
ln := ln + ', ';
ln := ln + IntToStr(i);
end;
lbLog.Items.Add('Generator: ' + ln);
end;


And now it really is a time to say goodbye. At least to this series - I intend to write many more blog entries. It was an interesting experience for me and I hope an interesting reading for you, dear reader. If you liked it, tell that to others so that they may enjoy it too.



[A full source code of the demo program including all enumerators is available at http://17slon.com/blogs/gabr/files/FunWithEnumerators.zip]





Technorati tags: , ,

Labels: , , ,

Friday, March 09, 2007

Fun with enumerators, part 5 - class helper enumerators

[Part 1 - Introduction, Part 2 - Additional enumerators, Part 3 - Parameterized enumerators, Part 4 - External enumerators.]

Last time we saw how to add enumeration to the classes we cannot modify by using global functions. Today I'll show you another way - enumerators can be added by using class helpers.

Class helpers offer us a way to extend classes that we cannot touch otherwise. For example, they are used inside Delphi 2007's VCL to add glass support to TCustomForm (it was impossible to extend TCustomForm itself as D2007 is a non-breaking release and must keep compatibility with D2006 DCUs and BPLs).

Class helpers are actually a heavy compiler trickstery. When you declare a class helper, you're not extending original virtual method table, you just get a new global function that takes a hidden parameter to 'Self' and uses it when calling other class helpers or methods from the original class. It's quite confusing and I don't want to dig much deeper in this direction. Suffice to say that class helpers allow you to create a very strong make-beliefe that new method was added to an existing class. (Some more data on class helpers can be found in Delphi help.)

Instead of writing new enumerator we'll be reusing existing TStringsEnumReversed and TStringsEnumReversedFactory from Part 4. We'll just add new class helper that will replace the EnumReversed global function.

  TStringsEnumReversedHelper = class helper for TStrings
public
function EnumReversed: IStringsEnumReversedFactory;
end;

function TStringsEnumReversedHelper.EnumReversed: IStringsEnumReversedFactory;
begin
Result := TStringsEnumReversedFactory.Create(Self);
end;


Believe it or not, that's it. We can now use EnumReversed as if it was a method of the TStrings class.

procedure TfrmFunWithEnumerators.btnReverseLogWithCHClick(Sender: TObject);
var
s : string;
sl: TStringList;
begin
sl := TStringList.Create;
for s in lbLog.Items.EnumReversed do
sl.Add(s);
lbLog.Items.Assign(sl);
sl.Free;
end;


That looks good, but what I'll show you next will be even better.

procedure TfrmFunWithEnumerators.btnClassHelperClick(Sender: TObject);
var
b : TBits;
ln: string;
i : integer;
begin
b := TBits.Create;
b[1] := true; b[3] := true; b[5] := true;
ln := '';
for i in b do
ln := ln + IntToStr(i);
lbLog.Items.Add('Class helper enumerator: ' + ln);
b.Free;
end;


Here we created an instance of the TBits class and then used standard enumeration pattern (check it out - it says for i in b do - no extra properties or functions are hiding here!) to get all set bits (1, 3, and 5). And what's so great here? Check the TBits definition in Classes.pas - it doesn't contain any enumerator!





Again, a class helper did the magic.

  TBitsEnumHelper = class helper for TBits
public
function GetEnumerator: TBitsEnum;
end;

function TBitsEnumHelper.GetEnumerator: TBitsEnum;
begin
Result := TBitsEnum.Create(Self);
end;


This time we injected GetEnumerator function directly into the base class. That removed the need for intermediate factory interface/class.



There are no special tricks in the enumerator definition.

  TBitsEnum = class
private
FOwner: TBits;
FIndex: integer;
public
constructor Create(owner: TBits);
function GetCurrent: integer;
function MoveNext: boolean;
property Current: integer read GetCurrent;
end;

constructor TBitsEnum.Create(owner: TBits);
begin
FOwner := owner;
FIndex := -1;
end;

function TBitsEnum.GetCurrent: integer;
begin
Result := FIndex;
end;

function TBitsEnum.MoveNext: boolean;
begin
Result := false;
while FIndex < (FOwner.Size-1) do begin
Inc(FIndex);
if FOwner[FIndex] then begin
Result := true;
break; //while
end;
end;
end;


Admit it, class helpers are great. They can also be great source of problems. Class helpers were introduced mainly for internal CodeGear use and they have one big limitation - at any given moment, there can be at most one helper active for a given class.





You can define and associate multiple class helpers with a single class type. However, only zero or one class helper applies in any specific location in source code. The class helper defined in the nearest scope will apply. Class helper scope is determined in the normal Delphi fashion (i.e. right to left in the unit's uses clause). [excerpt from Delphi help]



IOW, if Delphi already includes class helper for a class and you write another, you'll loose the Delphi-provided functionality. (You can inherit from the Delphi class helper though - read more in Delphi help.) Use class helpers with care!



Technorati tags: , ,

Labels: , , ,

Thursday, March 08, 2007

Fun with enumerators, part 4 - external enumerators

[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
//do something with enumerator.Current;
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
// do something with s


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.





Technorati tags: , ,

Labels: , , ,

Wednesday, March 07, 2007

Fun with enumerators, part 3 - parameterized enumerators

[Part 1 - Introduction, Part 2 - Additional enumerators.]

In first two installments of this series, we took a look at for..in statement, described Delphi support for custom enumerators and demonstrated one possible way to add additional enumerator to a class that already implements one.

Today we'll implement a parameterized enumerator. Enumerators of this kind can be especially handy when used as filters. For example, an enumerator that takes a class reference as a parameter can be used to filter a TObjectList containing objects of many different classes.

IOW, we would like to write a following construct: for obj in list.Enum(TSomeClass) do. Clearly, Enum cannot be a property anymore, but it can be a function.

We can still use an enumerator factory object stored inside our class (as in the Part 2). Besides that, we must implement enumeration function that will take one or more parameters, preserve them inside the factory object and return the factory object so that Delphi compiler can call its GetEnumerator function.

Sounds complicated but in reality it really is simple.

First we have to add an internal factory object and an enumeration function to the TCustomStringList class.

  TCustomStringList = class(TStringList)
private
FEnumEveryNth: TCSLEnumEveryNthFactory;
public
constructor Create;
destructor Destroy; override;
function SkipEveryNth(skip: integer): TCSLEnumEveryNthFactory;
end;

constructor TCustomStringList.Create;
begin
inherited;
FEnumEveryNth := TCSLEnumEveryNthFactory.Create(Self);
end;

destructor TCustomStringList.Destroy;
begin
FEnumEveryNth.Free;
inherited;
end;

function TCustomStringList.SkipEveryNth(skip: integer): TCSLEnumEveryNthFactory;
begin
FEnumEveryNth.Skip := skip;
Result := FEnumEveryNth;
end;


Enumeration function SkipEveryNth takes one parameter and passes it to the factory object. Then it returns this factory object.



We also need a new factory class. This is an extended version of factory class from Part 2. It must implement storage for all enumerator parameters. In this case, this storage is implemented via property Skip.

  TCSLEnumEveryNthFactory = class
private
FOwner: TCustomStringList;
FSkip: integer;
public
constructor Create(owner: TCustomStringList);
function GetEnumerator: TCSLEnumEveryNth;
property Skip: integer read FSkip write FSkip;
end;

constructor TCSLEnumEveryNthFactory.Create(owner: TCustomStringList);
begin
inherited Create;
FOwner := owner;
FSkip := 1;
end;

function TCSLEnumEveryNthFactory.GetEnumerator: TCSLEnumEveryNth;
begin
Result := TCSLEnumEveryNth.Create(FOwner, FSkip);
end;


As you can see, GetEnumerator was also changed - in addition to passing FOwner to the created enumerator, it also passes current value of the Skip property.



New enumerator is very similar to the one from the Part 2, except that it advances list index by the specified ammount (instead of advancing it by 2).

  TCSLEnumEveryNth = class
private
FOwner: TCustomStringList;
FListIndex: integer;
FSkip: integer;
public
constructor Create(owner: TCustomStringList; skip: integer);
function GetCurrent: string;
function MoveNext: boolean;
property Current: string read GetCurrent;
end;

constructor TCSLEnumEveryNth.Create(owner: TCustomStringList; skip: integer);
begin
FOwner := owner;
FListIndex := -skip;
FSkip := skip;
end;

function TCSLEnumEveryNth.GetCurrent: string;
begin
Result := FOwner[FListIndex];
end;

function TCSLEnumEveryNth.MoveNext: boolean;
begin
Inc(FListIndex, FSkip);
Result := (FListIndex < FOwner.Count);
end;


We can now write some test code.

procedure TfrmFunWithEnumerators.btnSkipEveryThirdClick(Sender: TObject);
var
ln: string;
s : string;
begin
ln := '';
for s in FslTest.SkipEveryNth(3) do
ln := ln + s;
lbLog.Items.Add('Parameterized enumerator: ' + ln);
end;


Delphi compiler will translate this for..in roughly into

enumerator := FslTest.SkipEveryNth(3).GetEnumerator;
while enumerator.MoveNext do
ln := ln + enumerator.Current;
enumerator.Free;


So what is going on here?





  1. FslTest.SkipEveryNth(3) sets  FslTest.FEnumEveryNth.Skip to 3 and returns FslTest.FEnumEveryNth.

  2. Compiler calls FslTest.FEnumEveryNth.GetEnumerator.

  3. FslTest.FEnumEveryNth,GetEnumerator calls TCSLEnumEveryNth.Create(FslTest, 3) and returns newly created object.

  4. Enumerator loops until MoveNext returns false.


Test code result:





Tomorrow we'll do something even more interesting - we'll add new enumerator to existing class without creating a derived class.





Technorati tags: , ,

Labels: , , ,

Tuesday, March 06, 2007

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)
end;


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);
var
ch: char;
begin
FslTest := TCustomStringList.Create;
for ch := 'a' to 'z' do
FslTest.Add(ch);
end;

procedure TfrmFunWithEnumerators.FormDestroy(Sender: TObject);
begin
FslTest.Free;
end;


To test it, we'll iterate over the list and collect all lines into a single line.

procedure TfrmFunWithEnumerators.btnStandardIteratorClick(Sender: TObject);
var
ln: string;
s : string;
begin
ln := '';
for s in FslTest do
ln := ln + s;
lbLog.Items.Add('Standard enumerator: ' + ln);
end;


And here's a result of this test code:



Standard enumerator



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;
while enumerator.MoveNext do
//do something with enumerator.Current;
enumerator.Free;


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
private
FOwner: TCustomStringList;
public
constructor Create(owner: TCustomStringList);
function GetEnumerator: TCSLEnumEverySecond;
end;

TCustomStringList = class(TStringList)
private
FEnumEverySecond: TCSLEnumEverySecondFactory;
public
constructor Create;
destructor Destroy; override;
property SkipEverySecond: TCSLEnumEverySecondFactory read FEnumEverySecond;
end;


Now we can write

procedure TfrmFunWithEnumerators.btnSkipEverySecondClick(Sender: TObject);
var
ln: string;
s : string;
begin
ln := '';
for s in FslTest.SkipEverySecond do
ln := ln + s;
lbLog.Items.Add('Additional enumerator: ' + ln);
end;


and Delphi compiler will translate it (approximately) into

enumerator := FslTest.SkipEverySecond.GetEnumerator;
while enumerator.MoveNext do
ln := ln + enumerator.Current;
enumerator.Free;


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).

constructor TCustomStringList.Create;
begin
inherited;
FEnumEverySecond := TCSLEnumEverySecondFactory.Create(Self);
end;

destructor TCustomStringList.Destroy;
begin
FEnumEverySecond.Free;
inherited;
end;


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);
begin
inherited Create;
FOwner := owner;
end;

function TCSLEnumEverySecondFactory.GetEnumerator: TCSLEnumEverySecond;
begin
Result := TCSLEnumEverySecond.Create(FOwner);
end;


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);
begin
FOwner := owner;
FListIndex := -2;
end;

function TCSLEnumEverySecond.GetCurrent: string;
begin
Result := FOwner[FListIndex];
end;

function TCSLEnumEverySecond.MoveNext: boolean;
begin
Inc(FListIndex, 2);
Result := (FListIndex < FOwner.Count);
end;


And the results are ...





That wasn't so hard, wasn't it? Tomorrow we'll look into an even niftier topic - enumerators with parameters.



Technorati tags: , ,

Labels: , , ,

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: , ,

Labels: , , ,