Search code examples
delphidelphi-xe6

What's the intended way to pass a list?


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.


Solution

  • 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.