Delphi: Self-Contained-Multiple-Enumerator-Record


Für ein aktuelles Projekt wollte ich eine Klasse mit mehreren Enumeratoren versehen und bin dafür zunächst im Internet auf die Suche gegangen und schließlich wieder einmal bei den (immer wieder interessanten) Enumerator-Artikeln auf The Delphi Geek gelandet, im speziellen Fun with enumerators, part 2 – multiple enumerators.

Das dort beschriebene Prinzip für mehrere Enumeratoren in einer Klasse ist recht simpel: Ein Property liefert eine Instanz einer Factory-Klasse zurück, die die benötigte GetEnumerator-Funktion bereitstellt. Dazu muss aber jede Instanz der eigentlichen Klasse (also die, die mehrere Enumeratoren bereitstellen soll), eine Instanz oder zumindest eine Referenz auf die Factory mitbekommen. Für mein Problem würde das recht viel Overhead bedeuten (kleine Klasse mit vielen Instanzen), auch dann, wenn der Enumerator gar nicht benutzt wird.

Meine Lösung: Ein Record der Enumerator und Factory gleichzeitig ist: Der Record bekommt alle Funktionen für einen Enumerator und zusätzlich eine GetEnumerator-Funktion die eine Referenz auf den Record selbst zurückgibt. Ich nenne sie Self-Contained-Multiple-Enumerator-Record.

Als Beispiel habe ich das Beispiel von The Delphi Geek aufgegriffen: Eine Stringliste mit einem zusätzlichen Enumerator, der nur jede zweite Zeile zurückliefert. Hier die Deklaration der Klasse mit Enumerator-Record:

TCustomStringList = class( TStringList )
private
  type TEvery2ndEnum = record
  private
    Index : Integer;
    FCSL : TCustomStringList;
  public
    constructor Create( ACSL : TCustomStringList );
    function GetCurrent() : String;
    function MoveNext() : Boolean;
    property Current : String read GetCurrent;
    function GetEnumerator() : TEvery2ndEnum;
  end;
private
  function GetEvery2ndEnum : TEvery2ndEnum;
public
  property Every2nd : TEvery2ndEnum read GetEvery2ndEnum;
end;

In der Implementierung wird ausgenutzt, dass die erweiterten Records auch über eine Self-Variable verfügen: Die „Factory“ ist gleichzeitig Enumerator und liefert bei GetEnumerator sich selbst zurück.

Die Verwendung des Enumerators könnte dann beispielsweise wie folgt aussehen:

{ TCustomStringList}
function TCustomStringList.GetEvery2ndEnum() : TEvery2ndEnum;
begin
  Result := TCustomStringList.TEvery2ndEnum.Create( self );
end;

{ TCustomStringList.TEdgeEnumerator }
constructor TCustomStringList.TEvery2ndEnum.Create( ACSL : TCustomStringList );
begin
  Index := -2;
  FCSL := ACSL;
end;

function TCustomStringList.TEvery2ndEnum.GetCurrent() : String;
begin
  Result := FCSL[ Index ];
end;

function TCustomStringList.TEvery2ndEnum.GetEnumerator() : TEvery2ndEnum;
begin
  Result := self;
end;

function TCustomStringList.TEvery2ndEnum.MoveNext() : Boolean;
begin
  Result := ( Index + 2 );
  INC( Index, 2 );
end;
for s in CSL.Every2nd do
  Memo1.Lines.Add( s );

Das ganze hat zwei Vorteile gegenüber der Lösung mit Factory-Klasse: Es ist kein zusätzlicher Pointer auf eine Factory-Instanz in jeder Instanz von TCustomStringList nötig, was Speicher spart. Und es muss nur ein Record statt zwei Klassen für jeden Enumerator implementiert werden, was das Erstellen der Enumeratoren vereinfacht.