Why do explicit interface calls on generics always call the base implementation?
Asked Answered
L

2

14

Why do explicit C# interface calls within a generic method that has an interface type constraint always call the base implementation?

For example, consider the following code:

public interface IBase
{
    string Method();
}

public interface IDerived : IBase
{
    new string Method();
}

public class Foo : IDerived
{
    string IBase.Method()
    {
        return "IBase.Method";
    }

    string IDerived.Method()
    {
        return "IDerived.Method";
    }
}

static class Program
{
    static void Main()
    {
        IDerived foo = new Foo();
        Console.WriteLine(foo.Method());
        Console.WriteLine(GenericMethod<IDerived>(foo));
    }

    private static string GenericMethod<T>(object foo) where T : class, IBase
    {
        return (foo as T).Method();
    }
}

This code outputs the following:

IDerived.Method
IBase.Method

Instead of what one might expect:

IDerived.Method
IDerived.Method

There seems to be no way (short of reflection) to call a hidden, more derived explicit interface implementation of a type decided at runtime.

EDIT: To be clear, the following if check evaluates to true in the GenericMethod call above:

if (typeof(T) == typeof(IDerived))

So the answer is not that T is always treated as IBase due to the generic type constraint "where T : class, IBase".

Lambrecht answered 22/7, 2016 at 21:45 Comment(4)
Why should it call IDerived.Method in the first place? Besides having the same name IBase.Method and IDerived.Method are not related.Sloppy
Your GenericMethod is specifying IBase?Reportorial
Explicit interface implementation is required here since Method() is ambiguous, there are two of them. If you like the second outcome better then just improve your design. Either remove IBase from IDerived or remove Method() from IDerived since IBase already demands it to be implemented. And a single Method() in the class implements both interfaces without ambiguity.Christeenchristel
And here is the reason that shadowing (new) is generally bad.Carrel
O
7

The key here is to remember that IBase.Method and IDerived.Method are two completely different methods. We just happened to give them similar names and signatures. Since anything that implements IDerived also implements IBase that means it will have two methods named Method taking no parameters. One belongs to IDerived and one belongs to IBase.

All the compiler knows when compiling GenericMethod is that the generic parameter will implement at least IBase, so it can only guarantee that the IBase.Method implementation exists. So that's the method that's called.

Unlike C++ templates, the generic substitution doesn't happen whenever the method is compiled (which with templates would happen once for every combination of template parameters that's used). Instead the method is compiled exactly once in such a way that any type can be substituted at runtime.

In your case the compiler emits IL for GenericMethod that looks something like this:

IL_0000:  ldarg.0     
IL_0001:  isinst      <T> 
IL_0006:  unbox.any   <T>
IL_000B:  box         <T>    
IL_0010:  callvirt    IBase.Method
IL_0015:  ret         

Notice it explicitly calls IBase.Method. There's no virtual/override relationship between that method and IDerived.Method so the base is all that's called, regardless of what type gets substituted for T in the runtime.

Opinion answered 22/7, 2016 at 22:20 Comment(0)
U
1

Adding to Kyle's answer, which I cannot do in a comment because I do not have sufficient reputation yet...

I think this is telling:

private static string GenericMethod<T>(T foo) where T : class, IBase
{
    return foo.Method() + " "  + typeof(T) + " " + typeof(Foo);
}

Removing object and having the parameter be a T, so the as-cast is unnecessary still calls the IBase.Method.

I’m pretty sure this is all directly due to 4.4.4 Satisfying Constraints in the C# specification.

C# generics do not behave like C++ templates in this regard.

Ulotrichous answered 25/7, 2016 at 15:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.