Why can't I pass a TObjectList<S: T> to a function expecting a TObjectList<T>?
Asked Answered
S

2

8

I have problem with my code, which uses generic types. Why doesn't the compiler know that the passed list (Result) is a TObjectList<TItem> (TItem is type for T in TItems)?

Interface:

type
   TItem = class
end;

type
  IItemsLoader = interface
    procedure LoadAll(AList : TObjectList<TItem>);
end;

type
  TItemsLoader = class(TInterfacedObject, IItemsLoader)
public
  procedure LoadAll(AList : TObjectList<TItem>);
end;

type
  IItems<T : TItem> = interface
  function LoadAll : TObjectList<T>;
end;

type
  TItems<T : TItem> = class(TInterfacedObject, IItems<T>)
  private
    FItemsLoader : TItemsLoader;
  public
    constructor Create;
    destructor Destroy; override;
    function LoadAll : TObjectList<T>;
end;

Implementation:

procedure TItemsLoader.LoadAll(AList: TObjectList<TItem>);
begin
  /// some stuff with AList
end;

{ TItems<T> }

constructor TItems<T>.Create;
begin
  FItemsLoader := TItemsLoader.Create;
end;

destructor TItems<T>.Destroy;
begin
  FItemsLoader.Free;
  inherited;
end;

function TItems<T>.LoadAll: TObjectList<T>;
begin
  Result := TObjectList<T>.Create();

  /// Error here
  /// FItemsLoader.LoadAll(Result);
end;
Scrivenor answered 27/4, 2012 at 16:47 Comment(0)
G
3

In the function with the error, Result is a TObjectList<T>, where T is some subclass of TItem, but the compiler doesn't know what specific class it is. The compiler has to compile it so that it's safe to run for any value of T. That might not be compatible with the argument type of LoadAll, which requires a TObjectList<TItem>, so the compiler rejects the code.

Suppose T is TItemDescendant, and the compiler allows the faulty code to compile and execute. If LoadAll calls AList.Add(TItem.Create), then AList will end up holding something that isn't a TItemDescendant, even though it's a TObjectList<TItemDescendant>. It holds an object of a type different from what its generic type parameter says it holds.

Just because S is a subtype of T doesn't mean that X<S> is a subtype of X<T>.

Gurnard answered 27/4, 2012 at 17:51 Comment(2)
You're right, I didn't looked at it from this side. Do you have any suggestion how to refactor this code? It's important for me to have interface like LoadAll and return result list or pass result list as parameter to TItems.LoadAll But real work is doing by TItemsLoader. TItems is only indirect layer. TItems "knows" what kind of items will have. TItemsLoader should only know, that items are TItem or its descendant.Scrivenor
I believe the other answer with IItemsLoader interface, is what you need to change to doing. However Rob has exactly explained it right and answered your direct question.Savonarola
A
4

You have to use a generic version of the Loader as well:

type
   TItem = class
end;

type
  IItemsLoader<T: TItem> = interface
    procedure LoadAll(AList : TObjectList<T>);
end;

type
  TItemsLoader<T: TItem> = class(TInterfacedObject, IItemsLoader<T>)
public
  procedure LoadAll(AList : TObjectList<T>);
end;

type
  IItems<T : TItem> = interface
  function LoadAll : TObjectList<T>;
end;

type
  TItems<T : TItem> = class(TInterfacedObject, IItems<T>)
  private
    FItemsLoader : TItemsLoader<T>;
  public
    constructor Create;
    destructor Destroy; override;
    function LoadAll : TObjectList<T>;
end;


implementation

{$R *.dfm}

procedure TItemsLoader<T>.LoadAll(AList: TObjectList<T>);
begin
  /// some stuff with AList
end;

{ TItems<T> }

constructor TItems<T>.Create;
begin
  FItemsLoader := TItemsLoader<T>.Create;
end;

destructor TItems<T>.Destroy;
begin
  FItemsLoader.Free;
  inherited;
end;

function TItems<T>.LoadAll: TObjectList<T>;
begin
  Result := TObjectList<T>.Create();

  /// Error here
  FItemsLoader.LoadAll(Result);
end;
Apostolic answered 27/4, 2012 at 21:46 Comment(2)
In my concept `loader' should work only on TItem. But I will try to change it.Scrivenor
It will work with TItem and also with any other subclass, since the generic definition has the constraint.Apostolic
G
3

In the function with the error, Result is a TObjectList<T>, where T is some subclass of TItem, but the compiler doesn't know what specific class it is. The compiler has to compile it so that it's safe to run for any value of T. That might not be compatible with the argument type of LoadAll, which requires a TObjectList<TItem>, so the compiler rejects the code.

Suppose T is TItemDescendant, and the compiler allows the faulty code to compile and execute. If LoadAll calls AList.Add(TItem.Create), then AList will end up holding something that isn't a TItemDescendant, even though it's a TObjectList<TItemDescendant>. It holds an object of a type different from what its generic type parameter says it holds.

Just because S is a subtype of T doesn't mean that X<S> is a subtype of X<T>.

Gurnard answered 27/4, 2012 at 17:51 Comment(2)
You're right, I didn't looked at it from this side. Do you have any suggestion how to refactor this code? It's important for me to have interface like LoadAll and return result list or pass result list as parameter to TItems.LoadAll But real work is doing by TItemsLoader. TItems is only indirect layer. TItems "knows" what kind of items will have. TItemsLoader should only know, that items are TItem or its descendant.Scrivenor
I believe the other answer with IItemsLoader interface, is what you need to change to doing. However Rob has exactly explained it right and answered your direct question.Savonarola

© 2022 - 2024 — McMap. All rights reserved.