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

No comments:

Post a Comment