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, 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()
. TheGetEnumerator()
method must return a class, interface, or record type. The class, interface, or record returned byGetEnumerator()
must contain a public instance method calledMoveNext()
. TheMoveNext()
method must return a Boolean. - The class, interface, or record returned byGetEnumerator()
must contain a public instance, read-only property calledCurrent
. The type of theCurrent
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?
GetEnumerator
?GetEnumerator
?Current
property?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"?
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.
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.
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)"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With