How to properly make an interface support iteration?
Asked Answered
I

2

6

How can I expose this TList from an interface, as either IEnumerator or IEnumerator<IFungibleTroll>? I am using Delphi XE.

Here's how far I got:

unit FungibleTrollUnit;

interface

uses
  Windows, Messages, SysUtils,
  Variants, Classes, Graphics,
  Controls, Forms,
  Generics.Collections;

type
  IFungibleTroll = interface
    ['{03536137-E3F7-4F9B-B1F5-2C8010A4D019}']
       function GetTrollName:String;
       function GetTrollRetailPrice:Double;
  end;


  TFungibleTrolls = class (TInterfacedObject,IEnumerable<IFungibleTroll>)
      protected
         FTrolls:TList<IFungibleTroll>;

      public
          // IEnumerable
          function GetEnumerator:IEnumerator<IFungibleTroll>;//
//          function GetEnumerator:IEnumerator; overload;

         // find/search app feature requires searching.
         // this

         function FindSingleItemByName(aName:String;patternMatch:Boolean):IFungibleTroll;
         function FindMultipleItemsByName(aName:String;patternMatch:Boolean):IEnumerable<IFungibleTroll>;
         function FindSingleItemByIdentifier(anIdentifer:String):IFungibleTroll;// use internal non-visible identifier to find an app.

         constructor Create;

         property Trolls:TList<IFungibleTroll> read FTrolls; // implements IEnumerable<IFungibleTroll>;??
  private
  end;




implementation


{ TFungibleTrolls }

constructor TFungibleTrolls.Create;
begin
         FTrolls := TList<IFungibleTroll>.Create;

end;

function TFungibleTrolls.FindMultipleItemsByName(aName: String;
  patternMatch: Boolean): IEnumerable<IFungibleTroll>;
begin

end;

function TFungibleTrolls.FindSingleItemByIdentifier(
  anIdentifer: String): IFungibleTroll;
begin

end;

function TFungibleTrolls.FindSingleItemByName(aName: String;
  patternMatch: Boolean): IFungibleTroll;
begin

end;



function TFungibleTrolls.GetEnumerator: IEnumerator<IFungibleTroll>;
begin
  result := FTrolls.GetEnumerator;
end;

//function TFungibleTrolls.GetEnumerator: IEnumerator;
//begin
//  result := FTrolls.GetEnumerator; // type IEnumerator<IFungibleTroll> or IEnumerator?
//end;


end.

I get stuck in one of three errors that I can't figure out how to solve:

[DCC Error] FungibleTrollUnit.pas(26): E2252 Method 'GetEnumerator' with identical parameters already exists -or-

[DCC Error] FungibleTrollUnit.pas(19): E2291 Missing implementation of interface method IEnumerable.GetEnumerator -or- [DCC Error] FungibleTrollUnit.pas(19): E2291 Missing implementation of interface method IEnumerable.GetEnumerator

It seems I must declare two forms of GetEnumerator, if I declare TFungibleTrolls to implement IEnumerable, but I can't seem to figure out how to do it, either with overloads, or without overloads, or using a "method resolution clause", like this:

      function IEnumerable.GetEnumerator = GetPlainEnumerator; // method resolution clause needed?
      function GetEnumerator:IEnumerator<IFungibleTroll>;
      function GetPlainEnumerator:IEnumerator;

This probably seems like a pretty basic use of IEnumerable, and making an Interface support iteration, and yet, I'm stuck.

Update: It seems when I try to do this without first declaring a List<T>, I am falling into a crack caused by the fact that IEnumerable<T> inherits from IEnumerable, and yet, instead of a single get enumerator method, my class must provide multiple ones, and because my class is not a generic, it can't "map itself" to IEnumerable's requirements directly unless I use a generic List<T> declaration. Marjan's sample works, when compiled into a project (.dproj+.dpr) but not when built into a package (.dproj+.dpk) and compiled in the IDE. It works fine from the command line, in a package, but not in the IDE, in a package.

Irritating answered 17/6, 2011 at 17:19 Comment(0)
D
5

Not an answer to your question directly (still working on that), but this is what I did to get an "interfaced enumerator", ie and interfaced class that supports iteration:

IList<T> = interface(IInterface)
  [...]
  function GetEnumerator: TList<T>.TEnumerator;
  function Add(const Value: T): Integer;
end;

type
  TBjmInterfacedList<T> = class(TBjmInterfacedObject, IList<T>)
  strict private
    FList: TList<T>;
    function GetEnumerator: TList<T>.TEnumerator;
  strict protected
    function Add(const Value: T): Integer;
  public
    constructor Create; override;
    destructor Destroy; override;
  end;

implementation

constructor TBjmInterfacedList<T>.Create;
begin
  inherited;
  FList := TList<T>.Create;
end;

destructor TBjmInterfacedList<T>.Destroy;
begin
  FreeAndNil(FList);
  inherited;
end;

function TBjmInterfacedList<T>.GetEnumerator: TList<T>.TEnumerator;
begin
  Result := FList.GetEnumerator;
end;

function TBjmInterfacedList<T>.Add(const Value: T): Integer;
begin
  Result := FList.Add(Value);
end;

And then you can do stuff like:

ISite = interface(IInterface)
  ...
end;
ISites = interface(IList<ISite>);
  ...
end;

var
  for Site in Sites do begin
    ...
  end;

with implementing classes like:

TSite = class(TBjmInterfacedObject, ISite)
  ...
end;
TSites = class(TBjmInterfacedList<ISite>, ISites)
  ...
end;

Update

Example project source uploaded to http://www.bjmsoftware.com/delphistuff/stackoverflow/interfacedlist.zip

Deservedly answered 17/6, 2011 at 17:55 Comment(11)
So was it a stylistic mistake for me to avoid the <T> generics declaration and just try to "drive straight at the target?"Irritating
Possibly. What you run into otherwise is indeed the need for two GetEnumerator functions without the means to overload them as they do not have distinguishing parameters... I was trying to find the help on for ... in support, but got twarthed by the D2010 help. One thing I am sure of is that to get for ... in support you do not need to declare support for IEnumerable or even IEnumerator. You just need to provide the compiler with the necessary methods...Deservedly
When I try to build this, I add what I hope I needed to add, and I get [DCC Fatal Error] unit1.pas(1): F2084 Internal Error: AV221EBFB0-R00000014-0.Irritating
Yikes. That sounds bad. I'll create a simple console project for you. Hang on.Deservedly
So the compiler, without recourse to generics, lacks a means to let you declare the required methods. Interesting. Really that should be in the docs somewhere! I have a reason to get over my dislike of generics, because they are now REQUIRED, if we are going to use interfaced-lists and expose them as iterable objects. I wasn't 100% sure until I tried to do this without any <T> stuff, that you can't.Irritating
Uploaded an example project for you. See link in update to my answer. And yes, it seems that <T> is unavoidable if you want to do something..., well..., generic... :-)Deservedly
And yes, I think they should not have declared IEnumerable<T> as a descendant of IEnumerable, because you have no way of creating a class with both required GetEnumerator methods... On the other hand, maybe you need that (need to think about it some more).Deservedly
Your example works great, and so does mine, except when I put your code, or my code, into a runtime package. When I build it as part of a package, the compiler fails with F2084 internal error: AV, which means, I think internal compiler access violation.Irritating
That sucks. What version of Delphi? My D2010 just built a package with just the unit added without problems. Tried runtime only, designtime only and both runtime and designtime. D2009 mutinies about "undeclared identifier GetName" in the TSite declaration and red-lines TList<T>.Enumerator in the TInterfacedList<T> declaration, but does not throw any internal errors.Deservedly
@Warren: you are probabley using D2009 right? I remember having tons of problems with generics in runtime packages. Delphi 2010 and XE fixed all of them.Surfing
Nope, XE. I just wrote on another thread that XE fixed all the package bugs. But apparently there are some left. It's reported, in their bug tracker, and the team is aware of the bug and so I expect it will get fixed.Irritating
C
3

If you really want to make a class that implements IEnumerable<T>, you can do it like this:

unit uGenericEnumerable;

interface

uses SysUtils, Classes, Generics.Collections;

type TGenericEnumerator<T> = class(TInterfacedObject, IEnumerator, IEnumerator<T>)
  private
    FList: TList<T>;
    FIndex: Integer;
  protected
    function GenericGetCurrent: T;
  public
    constructor Create(AList: TList<T>);
    procedure Reset;
    function MoveNext: Boolean;
    function GetCurrent: TObject;
    function IEnumerator<T>.GetCurrent = GenericGetCurrent;

    property Current: T read GenericGetCurrent;
end;

type TNonGenericEnumerable = class(TInterfacedObject, IEnumerable)
  protected
    function GetNonGenericEnumerator: IEnumerator; virtual; abstract;
  public
    function IEnumerable.GetEnumerator = GetNonGenericEnumerator;
end;

type TGenericEnumerable<T> = class(TNonGenericEnumerable, IEnumerable<T>)
  private
    FList: TList<T>;
  public
    constructor Create;
    destructor Destroy; override;
    function GetNonGenericEnumerator: IEnumerator; override;
    function GetEnumerator: IEnumerator<T>;
    property List: TList<T> read FList;
end;

implementation

{ TGenericEnumerator<T> }

constructor TGenericEnumerator<T>.Create(AList: TList<T>);
begin
  inherited Create;
  FList := AList;
  FIndex := -1;
end;

procedure TGenericEnumerator<T>.Reset;
begin
  FIndex := -1;
end;

function TGenericEnumerator<T>.MoveNext: Boolean;
begin
  if FIndex < FList.Count then
  begin
    Inc(FIndex);
    Result := FIndex < FList.Count;
  end
  else
  begin
    Result := False;
  end;
end;

function TGenericEnumerator<T>.GenericGetCurrent: T;
begin
  Result := FList[FIndex];
end;

function TGenericEnumerator<T>.GetCurrent: TObject;
begin
  // If T has not been constrained to being a class, raise an exception instead of trying to return an object.
  raise Exception.Create('Cannot use this as a non-generic enumerator');
  // If T has been constrained to being a class, return GenericGetCurrent.
  // Result := GenericGetCurrent;
end;


{ TGenericEnumerable<T> }

constructor TGenericEnumerable<T>.Create;
begin
  inherited Create;
  FList := TList<T>.Create;
end;

destructor TGenericEnumerable<T>.Destroy;
begin
  FList.Free;
end;

function TGenericEnumerable<T>.GetEnumerator: IEnumerator<T>;
begin
  Result := TGenericEnumerator<T>.Create(FList);
end;

function TGenericEnumerable<T>.GetNonGenericEnumerator: IEnumerator;
begin
  Result := GetEnumerator;
end;

end.

Now, your FungibleTrollUnit will look something like this:

unit FungibleTrollUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics,
  Controls, Forms, Generics.Collections,
  uGenericEnumerable;

type
  IFungibleTroll = interface
    ['{03536137-E3F7-4F9B-B1F5-2C8010A4D019}']
       function GetTrollName:String;
       function GetTrollRetailPrice:Double;
  end;

  IFungibleTrolls = interface(IEnumerable<IFungibleTroll>)
    ['{090B45FB-2925-4BFC-AE97-5D3F54E1C575}']
      function GetTrolls: TList<IFungibleTroll>;
      function FindSingleItemByName(aName:String):IFungibleTroll;
      function FindMultipleItemsByName(aName:String):IEnumerable<IFungibleTroll>;

      property Trolls:TList<IFungibleTroll> read GetTrolls;
  end;


  TFungibleTrolls = class (TGenericEnumerable<IFungibleTroll>, IFungibleTrolls, IEnumerable<IFungibleTroll>)
      public
         function GetTrolls: TList<IFungibleTroll>;

         function FindSingleItemByName(aName: String): IFungibleTroll;
         function FindMultipleItemsByName(aName: String): IEnumerable<IFungibleTroll>;

         property Trolls:TList<IFungibleTroll> read GetTrolls;
  private
  end;

implementation

uses StrUtils;

{ TFungibleTrolls }

function TFungibleTrolls.GetTrolls: TList<IFungibleTroll>;
begin
  Result := List;
end;

function TFungibleTrolls.FindMultipleItemsByName(aName: String): IEnumerable<IFungibleTroll>;
  var FilteredTrolls: TGenericEnumerable<IFungibleTroll>;
  var Troll: IFungibleTroll;
begin
  FilteredTrolls := TGenericEnumerable<IFungibleTroll>.Create;
  for Troll in List do
  begin
    if Troll.GetTrollName = aName then
      FilteredTrolls.List.Add(Troll);
  end;
  Result := IEnumerable<IFungibleTroll>(FilteredTrolls);
end;

function TFungibleTrolls.FindSingleItemByName(aName: String): IFungibleTroll;
  var Troll: IFungibleTroll;
begin
  Result := nil;
  for Troll in List do
  begin
    if Troll.GetTrollName = aName then
      Result := Troll;
      break;
  end;
end;

end.

Note that the implementation of IEnumerable does not work, but IEnumerable<T> does work.
This is because, unless T is constrained, you cannot convert a T to a TObject.
If T is a string or an integer, for example, then the IEnumerator does not have a TObject to return.

If T is constrained to be a class, you can get the implementation of IEnumerable to work.
If you constrain T to be an IInterface, you could get IEnumerable to work (Delphi 2010 and after (cast GenericGetCurrent to an IInterface, then to a TObject)), but I doubt if that is an advantage.

I would rather use it without the constraints, and do without being able to iterate everything as TObjects.

TGenericEnumerable<T>.GetEnumerator can't use FList.GetEnumerator because TList<T>.GetEnumerator does not return an IEnumerator<T>

Even though you can implement TGenericEnumerable<T> without defining TNonGenericEnumerable, like this:

type TGenericEnumerable<T> = class(TInterfacedObject, IEnumerable, IEnumerable<T>)
  private
    FList: TList<T>;
  protected
    function GenericGetEnumerator: IEnumerator<T>;
  public
    constructor Create;
    destructor Destroy; override;
    function GetEnumerator: IEnumerator;
    function IEnumerable<T>.GetEnumerator = GenericGetEnumerator;

    property List: TList<T> read FList;
end;

the disadvantage of doing this is that if you try to iterate using the TGenericEnumerable<T> object, rather than the interface, GetEnumerator will be non-generic and you can only iterate TObjects.

Usual caveats about mixing references to an interface and its underlying object. If you refer to an object both as an object type and as an IEnumerable<....>, then when the interface reference count goes back to zero, the object will be freed even if you still have a reference to it as an object. (That's why I defined IFungibleTrolls; so that I could refer to the collection as an interface).

You can make alternative implementations of TGenericEnumerator<T>; for example, it could contain a reference to a list, an index and a selection predicate, which are all supplied in the constructor.

Crossman answered 25/2, 2013 at 12:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.