What's the intended way to pass a list?
Asked Answered
H

2

7

I have an existing class, that has an existing method, that allows you to pass it a list of stuff:

TContoso = class(TSkyrim)
public
   procedure AddObjects(Objects: TList);
end;

And so, in the before-times, someone could pass a TList or a TObjectList to the method:

var
   list: TList;

list := TObjectList.Create(True);
contoso.AddObjects(list);

It didn't matter, as TObjectList was a TList. My method was flexible; it could take either.

Now in the after times

Now in the after times, i prefer typed lists:

var
   list: TList<TGrobber>;

list := TObjectList<TGrobber>.Create(True);
contoso.AddObjects(list);

Of course that doesn't compile, as neither TList<T> nor TObjectList<T> descend from TList. Which isn't such a problem. I intuitiavely understand that i don't actually need a TList, i just need something that is "enumerable":

Based on my experience in the .NET FCL, that means i simply need to declare the parameter is IEnumerable, because everything is enumerable:

  • IEnumerable<T> comes from IEnumerable
  • ICollection comes from IEnumerable
  • ICollection<T> comes from IEnumerable
  • IList comes from IEnumerable
  • IList<T> comes from IEnumerable
  • List comes from IEnumerable
  • List<T> comes from IEnumerable

So i would do something like:

TContoso = class(TSkyrim)
public
   procedure AddObjects(Objects: IEnumerable);
end;    

Except the Delphi BCL doesn't allow the polymorphism that .NET world allows; the things that are enumerable don't implement the IEnumerable interface:

TList = class(TObject)
public
   function GetEnumerator: TListEnumerator;
end;

TObjectList = class(TList);

TList<T> = class(TEnumerable<T>)
public
   function GetEnumerator: TEnumerator<T>;
end;

TObjectList<T> = class(TList<T>);

Without the typing, how does the compiler know a type is enumerable?

Delphi uses secret hard-coded magic.:

the class or interface must implement a prescribed collection pattern. A type that implements the collection pattern must have the following attributes: - The class or interface must contain a public instance method called GetEnumerator(). The GetEnumerator() method must return a class, interface, or record type. The class, interface, or record returned by GetEnumerator() must contain a public instance method called MoveNext(). The MoveNext() method must return a Boolean. - The class, interface, or record returned by GetEnumerator() must contain a public instance, read-only property called Current. The type of the Current property must be the type contained in the collection.

What is the way that the language designers intended me to use enumerables in my code?

  • What do i declare the type of paramater
  • how do i check for the presence of a method called GetEnumerator?
  • how do i call the method GetEnumerator?
  • how do i call the Current property?
  • how do i call the Next method?

For example:

TContoso = class(TSkyrim)
public
   procedure AddObjects(const Objects);
end;    

procedure TContoso.AddObjects(const Objects);
var
   o: TObject;
   enumerator: TObject;
   bRes: Boolean;
begin
   //for o in Objects do
   //   InternalAdd(nil, '', o);

   if not HasMethod(Objects, 'GetEnumerator') then
      Exit;

    enumerator := InvokeMethod(Objects, 'GetEnumerator');

    if not HasMethod(enumerator, 'MoveNext') then
       Exit;

    bRes := InvokeMethod(enumerator, 'MoveNext');

    while bRes do
    begin
       if HasMethod(enumerator, 'Current');
           InternallAdd(nil, '', InvokeMethod(enumerator, 'Current'));

       bRes := InvokeMethod(enumerator, 'MoveNext');
    end;
end;

What is the intended way to pass "an enumerable bag of stuff"?

Hack

TContoso = class(TSkyrim)
public
   procedure AddObjects(Objects: TList); overload;
   procedure AddObjects(Objects: TList<T>); overload;
end;    

There must be a reason the designers chose not to have IList implement IEnumerable. There must be a compile time mechanism to iterate a list. But what is that reason, and what is that way.

Hagio answered 2/11, 2014 at 16:42 Comment(3)
I don't think you can do this as is, at least using the standard RTL. The generic containers return typed enumerators - that is a TEnumerator<T>. The compiler understands how to use this but does a type check that your foreach variable is type compatible with T. However, someone may have produced a suitable set of generic container classes which have non-generic ancestor types containing the non-type dependent code since this is the technique for controlling ultimate compile size. It may be worth checking Spring4D latest release.Binturong
You could add an AsEnumerable method to the containers. But a better library (Spring) would come with what you need out of the box I suspect.Ternan
I believe it's a pretty good request to QC.Alberta
C
1

TObjectList<T> derives from TList<T>, so use that as your parameter, making the method itself a Generic if you need to support multiple object types in the list, and then use a for-in loop to enumerate the list (which also works for the non-Generic TList and various other container classes):

Iteration Over Containers Using For statements

Type
  TContoso = class(TSkyrim)
  public
    procedure AddObjects(Objects: TList); overload;
    procedure AddObjects<T: class>(Objects: TList<T>); overload;
  end;

procedure TContoso.AddObjects(Objects: TList);
var
  Obj: Pointer;
begin
  for Obj in Objects do
  begin
    // use TObject(Obj) as needed...
  end;
end;

procedure TContoso.AddObjects<T>(Objects: TList<T>);
var
  Obj: T;
begin
  for Obj in Objects do
  begin
    // use Obj as needed...
  end;
end;

var
   list: TList;

list := TObjectList.Create(True);
contoso.AddObjects(list);

var
  list: TList<TGrobber>;

list := TObjectList<TGrobber>.Create(True);
contoso.AddObjects<TGrobber>(list);

Let the compiler validate the presence of GetEnumerator() and the sub-methods of the returned enumerator class. Don't try to handle it manually (if you want to do that, you have to use RTTI for it). Besides, for-in loops have built-in support for other types of containers (arrays, strings, sets, and records) that do not expose GetEnumerator() but are otherwise enumerable.

Cephalic answered 13/6, 2015 at 22:15 Comment(0)
L
1

Obviously the classic VMT (Virtual Method Table) structure didn't support multiple inheritance, so they didn't want anything enumerable to have to descent from say a TEnumerable that was descending from TObject since that would be too restrictive.

So they used that hack with the specific method signature that had to be in your class, forcing you to use the HasMethod/InvokeMethod. So indeed your methods that get passed anything "enumerable" would then be of the form "procedure TContoso.AddObjects(const Objects)"

Ladyfinger answered 13/6, 2015 at 20:57 Comment(6)
Huh? Delphi has supported implementing multiple interfaces in a single class for a very long time. You don't need full multiple inheritance for that. And the existing IEnumerable is an interface, not some class TEnumerable like you suggest.Allerus
I'm not speaking of full multiple inheritance C++ style. I think when I first started using Delphi they didn't have interfaces supportLadyfinger
I know IEnumerable is an interface, but when you don't have interfaces support, you'd be forced to use a class, or use Reflection-style (or Introspection if you prefer) tricks if your runtime supports such and you want to be more flexible. BTW, have you seen this one (back from 2006)? blogs.encodo.ch/news/view_article.php?id=11Ladyfinger
edn.embarcadero.com/article/30125 - "The first version of Delphi to support interfaces was Delphi 3. But there was a way to use and develop COM interfaces even in Delphi 2. How was that possible? The answer is simple. If you ignore the fact that a class can implement more than one interface, you can think of an interface as a pure abstract class."Ladyfinger
" they didn't want anything enumerable to have to descent from say a TEnumerable". They could have made those things implement IEnumerable. No reason to descend from a TEnumerable.Hagio
interfaces didn't exist in Delphi from the start. So they designed without them and then they had to keep backwards compatibility obviouslyLadyfinger
C
1

TObjectList<T> derives from TList<T>, so use that as your parameter, making the method itself a Generic if you need to support multiple object types in the list, and then use a for-in loop to enumerate the list (which also works for the non-Generic TList and various other container classes):

Iteration Over Containers Using For statements

Type
  TContoso = class(TSkyrim)
  public
    procedure AddObjects(Objects: TList); overload;
    procedure AddObjects<T: class>(Objects: TList<T>); overload;
  end;

procedure TContoso.AddObjects(Objects: TList);
var
  Obj: Pointer;
begin
  for Obj in Objects do
  begin
    // use TObject(Obj) as needed...
  end;
end;

procedure TContoso.AddObjects<T>(Objects: TList<T>);
var
  Obj: T;
begin
  for Obj in Objects do
  begin
    // use Obj as needed...
  end;
end;

var
   list: TList;

list := TObjectList.Create(True);
contoso.AddObjects(list);

var
  list: TList<TGrobber>;

list := TObjectList<TGrobber>.Create(True);
contoso.AddObjects<TGrobber>(list);

Let the compiler validate the presence of GetEnumerator() and the sub-methods of the returned enumerator class. Don't try to handle it manually (if you want to do that, you have to use RTTI for it). Besides, for-in loops have built-in support for other types of containers (arrays, strings, sets, and records) that do not expose GetEnumerator() but are otherwise enumerable.

Cephalic answered 13/6, 2015 at 22:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.