Search code examples
arraysdelphidelphi-xe2enumeratorstatic-array

How to expose the built in enumerator for a private static array field in Delphi?


I am trying to expose the build in TEnumerator for a private static array.

Delphi itself allows to enumerate a static array directly (see below) so I suspect that Delphi creates an enumerator in the background for the static array and I am hoping that I will be able to create and expose the same enumerator in GetEnumerator method.

(I am using Delphi XE2).

program Project6;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Generics.Collections;

type
  TMyEnum = (meA, meB);

  TMyClass = class
  private
    FItems: array[TMyEnum] of Integer;
  protected
  public
    function GetEnumerator: TEnumerator<Integer>;
  end;

{ TMyClass }

function TMyClass.GetEnumerator: TEnumerator<Integer>;
begin
  // What is the simplies way of creating this enumerator?
end;

var
  myObj: TMyClass;
  i: Integer;

begin
  myObj := TMyClass.Create;
  try
    // This works but only in the same unit
    for i in myObj.FItems do
      WriteLn(i);

    for i in myObj do
      WriteLn(i);

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

Note that I can write a custom emulator like below. But I am trying to avoid this and expose the built in one.

TStaticArrayEnumerator<T> = class(TEnumerator<T>)
  private
    FCurrent: Pointer;
    FElementAfterLast: Pointer;
  protected
    function DoGetCurrent: T; override;
    function DoMoveNext: Boolean; override;
  public
    constructor Create(aArray: Pointer; aCount: Integer);
  end;

{ TStaticArrayEnumerator<T> }

constructor TStaticArrayEnumerator<T>.Create(aArray: Pointer; aCount: Integer);
begin
  // need to point Current before the first element (see comment in DoMoveNext)
  FCurrent := Pointer(NativeInt(aArray) - SizeOf(T));
  FElementSize := aElementSize;
  FElementAfterLast := Pointer(NativeInt(aArray) + aCount * SizeOf(T))
end;

function TStaticArrayEnumerator<T>.DoGetCurrent: T;
begin
  Result := T(FCurrent^);
end;

function TStaticArrayEnumerator<T>.DoMoveNext: Boolean;
begin
  // This method gets called before DoGetCurrent gets called the first time
  FCurrent := Pointer(NativeInt(FCurrent) + SizeOf(T));

  Result := not (FCurrent = FElementAfterLast);
end;

Solution

  • Note that I can write a custom emulator like below. But I am trying to avoid this and expose the built in one.

    You cannot. There is no type that represents the enumerator for an array. When you write a for..in loop over the elements of an array, the compiler deals with that by inlining a classic for loop.

    Consider this program:

    type
      TMyEnum = (enum1, enum2, enum3);
    
    var
      arr: array [TMyEnum] of Integer;
      i: Integer;
    
    begin
      for i in arr do
        Writeln(i);
      Readln;
    end.
    

    And the code that is generated:

    Project1.dpr.13: for i in arr do
    004060D7 BE9CAB4000       mov esi,$0040ab9c
    004060DC 33DB             xor ebx,ebx
    004060DE 8B3C9E           mov edi,[esi+ebx*4]
    Project1.dpr.14: Writeln(i);
    004060E1 A110784000       mov eax,[$00407810]
    004060E6 8BD7             mov edx,edi
    004060E8 E823DCFFFF       call @Write0Long
    004060ED E8FEDEFFFF       call @WriteLn
    004060F2 E869CCFFFF       call @_IOTest
    004060F7 43               inc ebx
    Project1.dpr.13: for i in arr do
    004060F8 83FB03           cmp ebx,$03
    004060FB 75E1             jnz $004060de
    Project1.dpr.15: Readln;
    004060FD A114784000       mov eax,[$00407814]
    00406102 E8E5D7FFFF       call @ReadLn
    00406107 E854CCFFFF       call @_IOTest
    

    Frankly, the best you can do is very similar to what you already have. The problem with what you already have is the heap allocation. Write your enumerator using a record rather than a class, like this:

    type
      TArrayEnumerator<T> = record
      strict private
        type
          P = ^T;
      strict private
        FArr: P;
        FIndex: Integer;
        FCount: Integer;
      public
        class function Initialize(const Arr: array of T): TArrayEnumerator<T>; static;
        function GetCurrent: T;
        function MoveNext: Boolean;
        property Current: T read GetCurrent;
      end;
    
    class function TArrayEnumerator<T>.Initialize(const Arr: array of T): TArrayEnumerator<T>;
    begin
      Result.FArr := @Arr[low(Arr)];
      Result.FIndex := -1;
      Result.FCount := Length(Arr);
    end;
    
    function TArrayEnumerator<T>.MoveNext: Boolean;
    begin
      Result := FIndex < FCount-1;
      if Result then
        inc(FIndex);
    end;
    
    function TArrayEnumerator<T>.GetCurrent: T;
    var
      Ptr: P;
    begin
      Ptr := FArr;
      inc(Ptr, FIndex);
      Result := Ptr^;
    end;
    

    And then your GetEnumerator is implemented like so:

    function TMyClass.GetEnumerator: TArrayEnumerator<Integer>;
    begin
      Result := TArrayEnumerator<Integer>.Initialize(FItems);
    end;