Search code examples
c#genericstypesinterfacecovariance

Generic type parameter covariance and multiple interface implementations


If I have a generic interface with a covariant type parameter, like this:

interface IGeneric<out T>
{
    string GetName();
}

And If I define this class hierarchy:

class Base {}
class Derived1 : Base{}
class Derived2 : Base{}

Then I can implement the interface twice on a single class, like this, using explicit interface implementation:

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

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

If I use the (non-generic)DoubleDown class and cast it to IGeneric<Derived1> or IGeneric<Derived2> it functions as expected:

var x = new DoubleDown();
IGeneric<Derived1> id1 = x;        //cast to IGeneric<Derived1>
Console.WriteLine(id1.GetName());  //Derived1
IGeneric<Derived2> id2 = x;        //cast to IGeneric<Derived2>
Console.WriteLine(id2.GetName());  //Derived2

However, casting the x to IGeneric<Base>, gives the following result:

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

I expected the compiler to issue an error, as the call is ambiguous between the two implementations, but it returned the first declared interface.

Why is this allowed?

(inspired by A class implementing two different IObservables?. I tried to show to a colleague that this will fail, but somehow, it didn't)


Solution

  • 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.