chuacw
chuacw

Reputation: 1863

Why is a class implementing an interface not compatible with the interface type when used in generics?

Does anyone know why there's an error on the indicated lines?

Seems like a compiler bug according to the Type Compatibility page. Wildcard is T1, and TStringWildcard2 is T2.

Also according to this

An expression of type T2 can be assigned to a variable of type T1 if the value of the expression falls in the range of T1 and at least one of the following conditions is satisfied: T1 is the IUnknown or IDispatch interface type and T2 is Variant or OleVariant. (The variant's type code must be varEmpty, varUnknown, or varDispatch if T1 is IUnknown, and varEmpty or varDispatch if T1 is IDispatch.)

If T1 is IUnknown and T2 is OleVariant, an expression of type T2 can be assigned to a variable of type T1 - therefore the second error line should compile as well.

ACollection2 := ACollection1; // theoretically should compile, but doesn't. Since LWildcard := TStringWildcard2.Create (compiles, since LWildcard is declared as a variable of the Wildcard interface, and TStringWildcard2 implements the Wildcard interface).

As such, therefore, ACollection2 (TDictionary<string, Wildcard> ) := ACollection1; (TDictionary<string, TStringWildcard2>) should compile right?

program TestCollections;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  System.SysUtils, System.Generics.Collections;
type
  Wildcard = interface
  ['{941128E5-D87C-4E3E-98D8-CF45EE6FEC09}']
    procedure Clear;
  end;
  Wildcard<T> = interface(Wildcard)
    function  getMatch: T;
  end;
  TParent = class(TInterfacedObject)
  end;
  TStringWildcard1 = class(TParent, Wildcard<string>)
    function  getMatch: string;
    procedure Clear;
  end;
  TStringWildcard2 = class(TParent, Wildcard, Wildcard<string>)
    function  getMatch: string;
    procedure Clear;
  end;

{ TStringWildcard1 }

procedure TStringWildcard1.Clear;
begin
end;

function TStringWildcard1.getMatch: string;
begin
  Result := 'String match 1';
end;

{ TStringWildcard2 }

procedure TStringWildcard2.Clear;
begin
end;

function TStringWildcard2.getMatch: string;
begin
  Result := 'String match 2';
end;

var
  LWildcard: Wildcard;
  ACollection1: TDictionary<string, TStringWildcard2>;
  ACollection2, ACollection3: TDictionary<string, Wildcard>;
  ACollection4: TDictionary<string, IUnknown>;
begin
  LWildcard := TStringWildCard2.Create; // <-- TStringWildcard2 compatible with Wildcard
// Since TStringWildcard2 is compatible with Wildcard, therefore, 
// ACollection1 should be compatible with ACollection2
  ACollection1 := TDictionary<string, TStringWildcard2>.Create;
  ACollection2 := ACollection1;
  ACollection4 := TDictionary<string, OleVariant>.Create;
end.

Upvotes: 0

Views: 617

Answers (2)

David Heffernan
David Heffernan

Reputation: 613461

There is no compiler bug here. The compiler is behaving as designed. The assignments fail because the types are indeed not compatible. Delphi generic types are invariant.

The documentation says:

Two instantiated generics are considered assignment compatible if the base types are identical (or are aliases to a common type) and the type arguments are identical.

Now, let's look at the first assignment that fails:

var
  ACollection1: TDictionary<string, TStringWildcard2>;
  ACollection2: TDictionary<string, Wildcard>;
....
ACollection2 := ACollection1;

This fails because the type arguments are not identical.

And for ACollection4 we have

var
  ACollection4: TDictionary<string, IUnknown>;
....
ACollection4 := TDictionary<string, OleVariant>.Create;

Again, the type arguments are not identical.


There's a very good reason for the language designers choosing to make generic types invariant. Consider the following example.

type
  TClass1 = class(TObject)
  end;
  TClass2 = class(TClass1)
  end;

var
  List1: TList<TClass1>;
  List2: TList<TClass2>;
....
List2 := TList<TClass2>.Create;
List1 := List2; // does not compile, but let's imagine that it did
List1.Add(TClass1.Create);

Since List1 and List2 are the same object, we have now succeeded in putting an object of type TClass1 into List2 which breaks the type system.

In fact your attempt to assign

ACollection2 := ACollection1;

illustrates this very issue. Suppose that assignment was valid and then you added to ACollection2 something that implemented Wildcard but that was not TStringWildcard2. Then that same thing would have been added to ACollection1 and all of a sudden you'd succeeded in adding to ACollection1 something that was not TStringWildcard2 and you've broken the type system.

In languages that support generic variance there need to be runtime checks to stop this happening. As it happens, only yesterday our very own Jon Skeet blogged on this very topic: Array covariance: not just ugly, but slow too. So, be careful what you wish for!


Because of this the Delphi designers elected to make Delphi generic types invariant.

Upvotes: 4

Lucero
Lucero

Reputation: 60276

It's not a bug, it's a reflection you made that is wrong. Using a compatible type as a generic type argument does not make the generic type compatible as well. This has to do with covariance and contravariance.

Upvotes: 1

Related Questions