Better way to implement filtered enumerator on TList<TMyObject>
Asked Answered
B

5

10

Using Delphi 2010, let's say I've got a class declared like this:

TMyList = TList<TMyObject>

For this list Delphi kindly provides us with an enumerator, so we can write this:

var L:TMyList;
    E:TMyObject;
begin
  for E in L do ;
end;

The trouble is, I'd like to write this:

var L:TMyList;
    E:TMyObject;
begin
  for E in L.GetEnumerator('123') do ;
end;

That is, I want the ability to provide multiple enumerators for the same list, using some criteria. Unfortunately the implementation of for X in Z requires the presence of a function Z.GetEnumerator, with no parameters, that returns the given enumerator! To circumvent this problem I'm defining an interface that implements the "GetEnumerator" function, then I implement a class that implements the interface and finally I write a function on TMyList that returns the interface! And I'm returning an interface because I don't want to be bothered with manually freeing the very simple class... Any way, this requires a LOT of typing. Here's how this would look like:

TMyList = class(TList<TMyObject>)
protected

  // Simple enumerator; Gets access to the "root" list
  TSimpleEnumerator = class
  protected
  public
    constructor Create(aList:TList<TMyObject>; FilterValue:Integer);

    function MoveNext:Boolean; // This is where filtering happens
    property Current:TTipElement;
  end;

  // Interface that will create the TSimpleEnumerator. Want this
  // to be an interface so it will free itself.
  ISimpleEnumeratorFactory = interface
    function GetEnumerator:TSimpleEnumerator;
  end;

  // Class that implements the ISimpleEnumeratorFactory
  TSimpleEnumeratorFactory = class(TInterfacedObject, ISimpleEnumeratorFactory)
    function GetEnumerator:TSimpleEnumerator;
  end;

public
  function FilteredEnum(X:Integer):ISimpleEnumeratorFactory;
end;

Using this I can finally write:

var L:TMyList;
    E:TMyObject;
begin
  for E in L.FilteredEnum(7) do ;
end;

Do you know a better way of doing this? Maybe Delphi does support a way of calling GetEnumerator with a parameter directly?

Later Edit:

I decided to use Robert Love's idea of implementing the enumerator using anonymous methods and using gabr's "record" factory to save yet an other class. This allows me to create a brand new enumerator, complete with code, using just a few lines of code in a function, no new class declaration required.

Here's how my generic enumerator is declared, in a library unit:

TEnumGenericMoveNext<T> = reference to function: Boolean;
TEnumGenericCurrent<T> = reference to function: T;

TEnumGenericAnonim<T> = class
protected
  FEnumGenericMoveNext:TEnumGenericMoveNext<T>;
  FEnumGenericCurrent:TEnumGenericCurrent<T>;
  function GetCurrent:T;
public
  constructor Create(EnumGenericMoveNext:TEnumGenericMoveNext<T>; EnumGenericCurrent:TEnumGenericCurrent<T>);

  function MoveNext:Boolean;
  property Current:T read GetCurrent;
end;

TGenericAnonEnumFactory<T> = record
public
  FEnumGenericMoveNext:TEnumGenericMoveNext<T>;
  FEnumGenericCurrent:TEnumGenericCurrent<T>;
  constructor Create(EnumGenericMoveNext:TEnumGenericMoveNext<T>;   EnumGenericCurrent:TEnumGenericCurrent<T>);
  function GetEnumerator:TEnumGenericAnonim<T>;
end;

And here's a way to use it. On any class I can add a function like this (and I'm intentionally creating an enumerator that doesn't use a List<T> to show the power of this concept):

type Form1 = class(TForm)
protected
  function Numbers(From, To:Integer):TGenericAnonEnumFactory<Integer>;  
end;

// This is all that's needed to implement an enumerator!
function Form1.Numbers(From, To:Integer):TGenericAnonEnumFactory<Integer>;
var Current:Integer;
begin
  Current := From - 1;
  Result := TGenericAnonEnumFactory<Integer>.Create(
    // This is the MoveNext implementation
    function :Boolean
    begin
      Inc(Current);
      Result := Current <= To;
    end
    ,
    // This is the GetCurrent implementation
    function :Integer
    begin
      Result := Current;
    end
  );
end;

And here's how I'd use this new enumerator:

procedure Form1.Button1Click(Sender: TObject);
var N:Integer;
begin
  for N in Numbers(3,10) do
    Memo1.Lines.Add(IntToStr(N));
end;
Bly answered 5/7, 2010 at 7:28 Comment(1)
Thanks for this, now implementing enumerator support for classes has gotten a lot easier. If you want to use something like 'for obj in list do', then just declare GetEnumerator() like this: 'function GetEnumerator : TEnumGenericAnonim<T>' and in the implementation just add '.GetEnumerator' at the very end of the Create statement.Rsfsr
S
4

Delphi For in loop support requires on of the following: (From the Docs)

  • Primitive types that the compiler recognizes, such as arrays, sets or strings
  • Types that implement IEnumerable
  • Types that implement the GetEnumerator pattern as documented in the Delphi Language Guide

If you look at Generics.Collections.pas you will find the implementation for TDictionary<TKey,TValue> where it has three enumerators for TKey, TValue, and TPair<TKey,TValue> types. Embarcadero shows that they have used verbose implementation.

You could do something like this:

unit Generics.AnonEnum;
interface
uses
 SysUtils,
 Generics.Defaults,
 Generics.Collections;

type

  TAnonEnumerator<T> = class(TEnumerator<T>)
  protected
    FGetCurrent : TFunc<TAnonEnumerator<T>,T>;
    FMoveNext : TFunc<TAnonEnumerator<T>,Boolean>;
    function DoGetCurrent: T; override;
    function DoMoveNext: Boolean; override;
  public
    Constructor Create(aGetCurrent : TFunc<TAnonEnumerator<T>,T>;
                       aMoveNext : TFunc<TAnonEnumerator<T>,Boolean>);
  end;

  TAnonEnumerable<T> = class(TEnumerable<T>)
  protected
    FGetCurrent : TFunc<TAnonEnumerator<T>,T>;
    FMoveNext : TFunc<TAnonEnumerator<T>,Boolean>;
    function DoGetEnumerator: TEnumerator<T>; override;
  public
    Constructor Create(aGetCurrent : TFunc<TAnonEnumerator<T>,T>;
                       aMoveNext : TFunc<TAnonEnumerator<T>,Boolean>);
  end;

implementation

{ TEnumerable<T> }

constructor TAnonEnumerable<T>.Create(aGetCurrent: TFunc<TAnonEnumerator<T>, T>;
  aMoveNext: TFunc<TAnonEnumerator<T>, Boolean>);
begin
  FGetCurrent := aGetCurrent;
  FMoveNext := aMoveNext;
end;

function TAnonEnumerable<T>.DoGetEnumerator: TEnumerator<T>;
begin
 result := TAnonEnumerator<T>.Create(FGetCurrent,FMoveNext);
end;


{ TAnonEnumerator<T> }

constructor TAnonEnumerator<T>.Create(aGetCurrent: TFunc<TAnonEnumerator<T>, T>;
  aMoveNext: TFunc<TAnonEnumerator<T>, Boolean>);
begin
  FGetCurrent := aGetCurrent;
  FMoveNext := aMoveNext;
end;

function TAnonEnumerator<T>.DoGetCurrent: T;
begin
  result := FGetCurrent(self);
end;

function TAnonEnumerator<T>.DoMoveNext: Boolean;
begin
 result := FMoveNext(Self);
end;

end.

This would allow you declare your Current and MoveNext methods anonymously.

Superorder answered 5/7, 2010 at 9:54 Comment(1)
The idea of using anonymous methods to implement the enumerator is absolutely brilliant, it saves me from creating countless classes to implement different enumerators. After all the enumerator's code is usually pretty simple and it can be implemented in just a few lines of code. Thank you.Bly
L
8

See DeHL ( http://code.google.com/p/delphilhlplib/ ). You can write code that looks like this:

for E in List.Where(...).Distinct.Reversed.Take(10).Select(...)... etc. 

Just like you can do in .NET (no syntax linq of course).

Luben answered 5/7, 2010 at 9:46 Comment(2)
Thank you for the link. Even if I do eventually end up using DeHL I'll favor Robert Love's answer because it show a language feature that allows me to implement this myself.Bly
Sure. DeHL is there to save you time if you don't want to invest in thee kind of things. If you prefer writing your own lightweight implementation, then Robert's answer is the one.Luben
F
6

You approach is fine. I don't know of any better way.

Enumerator factory can also be implemented as a record instead of an interface.

Maybe you'll get some ideas here.

Foxed answered 5/7, 2010 at 10:22 Comment(1)
Using a record instead of an interface saves me yet an other "class". Thanks.Bly
S
4

Delphi For in loop support requires on of the following: (From the Docs)

  • Primitive types that the compiler recognizes, such as arrays, sets or strings
  • Types that implement IEnumerable
  • Types that implement the GetEnumerator pattern as documented in the Delphi Language Guide

If you look at Generics.Collections.pas you will find the implementation for TDictionary<TKey,TValue> where it has three enumerators for TKey, TValue, and TPair<TKey,TValue> types. Embarcadero shows that they have used verbose implementation.

You could do something like this:

unit Generics.AnonEnum;
interface
uses
 SysUtils,
 Generics.Defaults,
 Generics.Collections;

type

  TAnonEnumerator<T> = class(TEnumerator<T>)
  protected
    FGetCurrent : TFunc<TAnonEnumerator<T>,T>;
    FMoveNext : TFunc<TAnonEnumerator<T>,Boolean>;
    function DoGetCurrent: T; override;
    function DoMoveNext: Boolean; override;
  public
    Constructor Create(aGetCurrent : TFunc<TAnonEnumerator<T>,T>;
                       aMoveNext : TFunc<TAnonEnumerator<T>,Boolean>);
  end;

  TAnonEnumerable<T> = class(TEnumerable<T>)
  protected
    FGetCurrent : TFunc<TAnonEnumerator<T>,T>;
    FMoveNext : TFunc<TAnonEnumerator<T>,Boolean>;
    function DoGetEnumerator: TEnumerator<T>; override;
  public
    Constructor Create(aGetCurrent : TFunc<TAnonEnumerator<T>,T>;
                       aMoveNext : TFunc<TAnonEnumerator<T>,Boolean>);
  end;

implementation

{ TEnumerable<T> }

constructor TAnonEnumerable<T>.Create(aGetCurrent: TFunc<TAnonEnumerator<T>, T>;
  aMoveNext: TFunc<TAnonEnumerator<T>, Boolean>);
begin
  FGetCurrent := aGetCurrent;
  FMoveNext := aMoveNext;
end;

function TAnonEnumerable<T>.DoGetEnumerator: TEnumerator<T>;
begin
 result := TAnonEnumerator<T>.Create(FGetCurrent,FMoveNext);
end;


{ TAnonEnumerator<T> }

constructor TAnonEnumerator<T>.Create(aGetCurrent: TFunc<TAnonEnumerator<T>, T>;
  aMoveNext: TFunc<TAnonEnumerator<T>, Boolean>);
begin
  FGetCurrent := aGetCurrent;
  FMoveNext := aMoveNext;
end;

function TAnonEnumerator<T>.DoGetCurrent: T;
begin
  result := FGetCurrent(self);
end;

function TAnonEnumerator<T>.DoMoveNext: Boolean;
begin
 result := FMoveNext(Self);
end;

end.

This would allow you declare your Current and MoveNext methods anonymously.

Superorder answered 5/7, 2010 at 9:54 Comment(1)
The idea of using anonymous methods to implement the enumerator is absolutely brilliant, it saves me from creating countless classes to implement different enumerators. After all the enumerator's code is usually pretty simple and it can be implemented in just a few lines of code. Thank you.Bly
M
3

You can do away with the factory and the interface if you add a GetEnumerator() function to your enumerator, like this:

TFilteredEnum = class
public
  constructor Create(AList:TList<TMyObject>; AFilterValue:Integer);

  function GetEnumerator: TFilteredEnum;

  function MoveNext:Boolean; // This is where filtering happens
  property Current: TMyObject;
end;

and just return self:

function TFilteredEnum.GetEnumerator: TSimpleEnumerator;
begin
  result := Self;
end;

and Delphi will conveniently clean up your instance for you, just like it does any other enumerator:

var 
  L: TMyList;
  E: TMyObject;
begin
  for E in TFilteredEnum.Create(L, 7) do ;
end;

You can then extend your enumerator to use an anonymous method, which you can pass in the constructor:

TFilterFunction = reference to function (AObject: TMyObject): boolean;

TFilteredEnum = class
private
  FFilterFunction: TFilterFunction;
public
  constructor Create(AList:TList<TMyObject>; AFilterFunction: TFilterFunction);

  ...
end;

...

function TFilteredEnum.MoveNext: boolean;
begin
  if FIndex >= FList.Count then
    Exit(False);
  inc(FIndex);
  while (FIndex < FList.Count) and not FFilterFunction(FList[FIndex]) do
    inc(FIndex);
  result := FIndex < FList.Count;
end;

call it like this:

var 
  L:TMyList;
  E:TMyObject;
begin
  for E in TFilteredEnum.Create(L, function (AObject: TMyObject): boolean
                                   begin
                                     result := AObject.Value = 7;
                                   end;
                                ) do 
  begin
    //do stuff here
  end
end;

Then you could even make it a generic, but I wont do that here, my answer is long enough as it is.

N@

Mcclenon answered 6/7, 2010 at 2:23 Comment(0)
H
1

I use this approach...where the AProc performs the filter test.

TForEachDataItemProc = reference to procedure ( ADataItem: TDataItem; var AFinished: boolean );

procedure TDataItems.ForEachDataItem(AProc: TForEachDataItemProc);
var
  AFinished: Boolean;
  ADataItem: TDataItem;
begin
  AFinished:= False;
  for ADataItem in FItems.Values do
  begin
    AProc( ADataItem, AFinished );
    if AFinished then
      Break;
  end;
end;
Hash answered 5/7, 2010 at 9:23 Comment(2)
Thanks Nige, but this is not answering my question. I asked for ways to implement the enumerator because I want to use the "for E in L" loop.Bly
This is a good idea (although not completely relevant to the question) and really doesn't deserve downvotes.Foxed

© 2022 - 2024 — McMap. All rights reserved.