Generic type parameter covariance and multiple interface implementations

If you have tested both of:

class DoubleDown: IGeneric<Derived1>, IGeneric<Derived2> {
    string IGeneric<Derived1>.GetName() {
        return "Derived1";
    }

    string IGeneric<Derived2>.GetName() {
        return "Derived2";
    }
}

class DoubleDown: IGeneric<Derived2>, IGeneric<Derived1> {
    string IGeneric<Derived1>.GetName() {
        return "Derived1";
    }

    string IGeneric<Derived2>.GetName() {
        return "Derived2";
    }
}

You must have realized that the results in reality, changes with the order you declaring the interfaces to implement. But I'd say it is just unspecified.

First off, the specification(§13.4.4 Interface mapping) says:

  • If more than one member matches, it is unspecified which member is the implementation of I.M.
  • This situation can only occur if S is a constructed type where the two members as declared in the generic type have different signatures, but the type arguments make their signatures identical.

Here we have two questions to consider:

  • Q1: Do your generic interfaces have different signatures?
    A1: Yes. They are IGeneric<Derived2> and IGeneric<Derived1>.

  • Q2: Could the statement IGeneric<Base> b=x; make their signatures identical with type arguments?
    A2: No. You invoked the method through a generic covariant interface definition.

Thus your call meets the unspecified condition. But how could this happen?

Remember, whatever the interface you specified to refer the object of type DoubleDown, it is always a DoubleDown. That is, it always has these two GetName method. The interface you specify to refer it, in fact, performs contract selection.

The following is the part of captured image from the real test

enter image description here

This image shows what would be returned with GetMembers at runtime. In all cases you refer it, IGeneric<Derived1>, IGeneric<Derived2> or IGeneric<Base>, are nothing different. The following two image shows more details:

enter image description hereenter image description here

As the images shown, these two generic derived interfaces have neither the same name nor another signatures/tokens make them identical.


The compiler can't throw an error on the line

IGeneric<Base> b = x;
Console.WriteLine(b.GetName());   //Derived1

because there is no ambiguity that the compiler can know about. GetName() is in fact a valid method on interface IGeneric<Base>. The compiler doesn't track the runtime type of b to know that there is a type in there which could cause an ambiguity. So it's left up to the runtime to decide what to do. The runtime could throw an exception, but the designers of the CLR apparently decided against that (which I personally think was a good decision).

To put it another way, let's say that instead you simply had written the method:

public void CallIt(IGeneric<Base> b)
{
    string name = b.GetName();
}

and you provide no classes implementing IGeneric<T> in your assembly. You distribute this and many others implement this interface only once and are able to call your method just fine. However, someone eventually consumes your assembly and creates the DoubleDown class and passes it into your method. At what point should the compiler throw an error? Surely the already compiled and distributed assembly containing the call to GetName() can't produce a compiler error. You could say that the assignment from DoubleDown to IGeneric<Base> produces the ambiguity. but once again we could add another level of indirection into the original assembly:

public void CallItOnDerived1(IGeneric<Derived1> b)
{
    return CallIt(b); //b will be cast to IGeneric<Base>
}

Once again, many consumers could call either CallIt or CallItOnDerived1 and be just fine. But our consumer passing DoubleDown also is making a perfectly legal call that could not cause a compiler error when they call CallItOnDerived1 as converting from DoubleDown to IGeneric<Derived1> should certainly be OK. Thus, there is no point at which the compiler can throw an error other than possibly on the definition of DoubleDown, but this would eliminate the possibility of doing something potentially useful with no workaround.

I have actually answered this question more in depth elsewhere, and also provided a potential solution if the language could be changed:

No warning or error (or runtime failure) when contravariance leads to ambiguity

Given that the chance of the language changing to support this is virtually zero, I think that the current behavior is alright, except that it should be laid out in the specifications so that all implementations of the CLR would be expected to behave the same way.