Search code examples
c#interfacepolymorphismcontravariancemethod-resolution-order

Contravariant interfaces method dispatch/selection C#


Consider the following code:

interface ITest<in T>
{
    void DoTest(T instance);
}

class A {}
class B : A {}
class C : B {}

class Test : ITest<A>, ITest<B>
{
    void ITest<A>.DoTest(A instance)
    {
        Console.WriteLine(MethodInfo.GetCurrentMethod());
    }
    
    void ITest<B>.DoTest(B instance)
    {
        Console.WriteLine(MethodInfo.GetCurrentMethod());
    }
}

public class Program
{
    public static void Main()
    {
        ITest<C> test = new Test();
        test.DoTest(new C());
    }
}

The output is:

Void ITest<A>.DoTest(A)

To me this is not the expected behavior, or at least not the one most developers are expecting. The expected output is:

Void ITest<B>.DoTest(B)

Here the "best" implementation should be used, with best meaning the generic interface with the most derived type parameter up to the contravariant "static" type.

Instead it seems the "worst" is chosen.

Inspecting the generated IL doesn't unveil the selection mechanism as the call is dispatched through the correct static type so I assume it's up to the CLR to select the implementation:

.method public hidebysig static 
    void Main () cil managed 
{
    // Method begins at RVA 0x207c
    // Code size 20 (0x14)
    .maxstack 2
    .locals init (
        [0] class ITest`1<class C>
    )

    IL_0000: nop
    IL_0001: newobj instance void Test::.ctor()
    IL_0006: stloc.0
    IL_0007: ldloc.0
    IL_0008: newobj instance void C::.ctor()
    IL_000d: callvirt instance void class ITest`1<class C>::DoTest(!0)
    IL_0012: nop
    IL_0013: ret
} // end of method Program::Main

Why it acts like this? What are the rules in this case? How does it work behind the scenes?


Solution

  • This is specified in Section II.12.2 of ECMA-335.

    Relevant snippets of the spec follows:

    Where there are multiple implementations for a given interface method due to differences in type parameters, the declaration order of the interfaces on the class determines which method is invoked.
    ...
    The inheritance/implements tree for a type T is the n-ary tree formed as follows:

    • The root of the tree is T
      ...
    • If T has one or more explicit interfaces, Ix, then the inheritance/implements tree for each Ix is a child of the root node, in order.

    The type declaration order of the interfaces and super classes of a type T is the postorder depth-first traversal of the inheritance/implements tree of type T with any second and subsequent duplicates of any type omitted. Occurrences of the same interface with different type parameters are not considered duplicates

    So we've defined the type declaration order as the order in which these interfaces appear in the class declaration.

    When an interface method is invoked, the VES shall use the following algorithm to determine the appropriate method to call:

    • Beginning with the runtime class of the instance through which the interface method is invoked, using its interface table as constructed above, and substituting generic arguments, if any, specified on the invoking class:
      1. For each method in the list associated with the interface method, if there exists a method whose generic type arguments match exactly for this instantiation (or there are no generic type parameters), then call the first method
      2. Otherwise, if there exists a method in the list whose generic type parameters have the correct variance relationship, then call the first such method in the list

    This is saying that if there's no exact match, then take the first method in the type declaration order whose type parameters have the correct variance relationship.

    Your example appears as Case 6 in the section "II.12.2.1 Interface Implementation Examples", where the type S4<V> implements both IVarImpl (which means implementing IVar<A>) and IVar<B>, and the example shows that calling the method IVar<C>::P(C) on an instance of S4<A> results in the method S1<A,B>::P(!0:A) (that is, void P(A)) being called.

    Indeed, if we swap the order of ITest<A> and ITest<B> in the declaration of Test, we can see that the ITest<B>.DoTest(B instance) implementation ends up being called.