views:

161

answers:

2

Basically, I want a class to be able to implement two different versions of the same generic interface.

Consider this code

type
  // a generic interface
  ITest<T> = interface
    ['{6901FE04-8FCC-4181-9E92-85B73264B5DA}']
    function Val: T;
  end;

  // a class that purports to implement two different types of that interface
  TTest<T1, T2> = class(TInterfacedObject, ITest<T1>, ITest<T2>)
  protected
    fV1: T1;
    fV2: T2;
  public
    constructor Create(aV1: T1; aV2: T2);
    function Val: T1;               // Val() for ITest<T1>
    function T2Val: T2;             // Val() for ITest<T2>
    function ITest<T2>.Val = T2Val; // mapping
  end;

constructor TTest<T1, T2>.Create(aV1: T1; aV2: T2);
begin
  inherited Create;
  fV1 := aV1;
  fV2 := aV2;
end;

function TTest<T1, T2>.T2Val: T2;
begin
  result := fV2;
end;

function TTest<T1, T2>.Val: T1;
begin
  result := fV1;
end;

/////////////
procedure Test;
var
  t : TTest<integer, string>;
begin
  t := TTest<integer, string>.Create(39, 'Blah');
  ShowMessage((t as ITest<string>).Val);            // this works as expected
  ShowMessage(IntToStr((t as ITest<integer>).Val)); // this gets AV
end;

The first ShowMessage displays 'Blah' as I would expect, but the second crashes. The reason it crashes is because the call invokes T2Val() instead of Val() as I would have expected. Apparently the conflict resolution mapping maps the method for both types of interfaces and not just for ITest: T2.

So, here's my question(s).

Is this a bug? By which, I mean, did Embarcadero intend for this to be supported and simply implement it wrong? Or did they never have any intention of allowing programmers to do something like this at all? (Honestly, I was a little surprised that my test program even compiled)

If this is a bug, does anyone have any idea if there might be a workaround to let me have one class support two different types of a single generic interface?

+3  A: 

This is an interesting problem. What appears to be happening is that the compiler is always mapping the interface to the last specified version of the interface (if you swap the order then it calls the other method). This might have to do with the fact that both interfaces have the same GUID signature, so the dispatcher is getting confused as to which method should be invoked when it sees the interface call.

This appears to be a bug, so should be reported via quality central.

skamradt
+9  A: 

as with an interface type uses an interface cast, which uses the GUID to find the interface. For a generic interface with a GUID, every instantiation gets the same GUID. If a single type implements multiple copies of the interface, then looking up by GUID will result in the first interface being returned.

The program works as expected if you don't use an interface cast, and instead use an interface conversion like this:

procedure Test;
var
  t : TTest<integer, string>;
begin
  t := TTest<integer, string>.Create(39, 'Blah');
  ShowMessage(ITest<string>(t).Val);
  ShowMessage(IntToStr(ITest<Integer>(t).Val));
end;

Originally, when generics were being implemented for Win32, GUIDs were not permitted on generic interfaces. However, dynamic querying for generic interfaces was desirable for generic container scenarios, and in general, as a mechanism for querying a service provider for type-specific services in the context of an algorithm (such as sorting or searching, which require things like comparers and equality tests). So a new plan was formed: have a GUID on the generic interface, but create a hash of the type arguments for generic instantiations and fold (e.g. xor) the hash into GUID to create a unique GUID for every distinct and incompatible instantiation. However, this was late in the day, and a good implementation was not possible within time constraints. But the requirement for dynamic querying remained, so the GUIDs stayed. That's why things are the way they are today.

To solve your specific scenario, the best I can recommend is to use distinct descendants with explicit GUIDs; or use a different mechanism for querying for the interface.

Barry Kelly
Huh. Good to know. Thanks. You're just a gold mine of information Barry. Thank you for taking the time.
TrespassersW
D2010 has already this hash, Barry?
Fabricio Araujo
It does not, unfortunately. I really should look into it though.
Barry Kelly