We believe this example exhibits a bug in the C# compiler (do make fun of me if we are wrong). This bug may be well-known: After all, our example is a simple modification of what is described in this blog post.
using System;
namespace GenericConflict
{
class Base<T, S>
{
public virtual int Foo(T t)
{ return 1; }
public virtual int Foo(S s)
{ return 2; }
public int CallFooOfT(T t)
{ return Foo(t); }
public int CallFooOfS(S s)
{ return Foo(s); }
}
class Intermediate<T, S> : Base<T, S>
{
public override int Foo(T t)
{ return 11; }
}
class Conflict : Intermediate<string, string>
{
public override int Foo(string t)
{ return 101; }
}
static class Program
{
static void Main()
{
var conflict = new Conflict();
Console.WriteLine(conflict.CallFooOfT("Hello mum"));
Console.WriteLine(conflict.CallFooOfS("Hello mum"));
}
}
}
The idea is simply to create a class Base<T, S> with two virtual methods whose signatures will become identical after an ‘evil’ choice of T and S. The class Conflict overloads only one of the virtual methods, and because of the existence of Intermediate<,>, it should be well-defined which one!
But when the program is run, the output seems to show that the wrong overload was overridden.
When we read Sam Ng’s follow-up post we get the expression that that bug was not fixed because they believed a type-load exception would always be thrown. But in our example the code compiles and runs with no errors (just unexpected output).
Addition in 2020: This was corrected in later versions of the C# compiler (Roslyn?). When I asked this question, the output was:
11
101
As of 2020, tio.run gives this output:
101
2
Let’s do what we should always do when exhibiting a compiler bug: carefully contrast the expected and observed behaviours.
The observed behaviour is that the program produces 11 and 101 as the first and second outputs, respectively.
What is the expected behaviour? There are two “virtual slots”. The first output should be the result of calling the method in the
Foo(T)slot. The second output should be the result of calling the method in theFoo(S)slot.What goes in those slots?
In an instance of
Base<T,S>thereturn 1method goes in theFoo(T)slot, and thereturn 2method goes in theFoo(S)slot.In an instance of
Intermediate<T,S>thereturn 11method goes in theFoo(T)slot and thereturn 2method goes in theFoo(S)slot.Hopefully so far you agree with me.
In an instance of
Conflict, there are four possibilities:return 11method goes in theFoo(T)slot and thereturn 101method goes in theFoo(S)slot.return 101method goes in theFoo(T)slot and thereturn 2method goes in theFoo(S)slot.return 101method goes in both slots.You expect that one of two things will happen here, based on section 10.6.4 of the specification. Either:
Conflictoverrides the method inIntermediate<string, string>, because the method in the intermediate class is found first. In this case, possibility two is the correct behaviour. Or:Conflictis ambiguous as to which original declaration it overrides, and therefore possibility four is the correct one.In neither case is possibility one correct.
It is not 100% clear, I admit, which of these two is correct. My personal feeling is that the more sensible behaviour is to treat an overriding method as a private implementation detail of the intermediate class; the relevant question to my mind is not whether the intermediate class overrides a base class method, but rather whether it declares a method with a matching signature. In that case the correct behaviour would be to pick possibility four.
What the compiler actual does is what you expect: it picks possibility two. Because the intermediate class has a member which matches, we choose it as “the thing to override”, regardless of the fact that the method is not declared in the intermediate class. The compiler determines that
Intermediate<string, string>.Foois the method overridden byConflict.Foo, and emits the code accordingly. It does not produce an error because it judges that the program is not in error.So if the compiler is correctly analyzing the code, choosing possibility two, and not producing an error, then why at runtime does it appear that the compiler chose possibility one, not possibility two?
Because making a program that causes two methods to unify under generic construction is implementation-defined behaviour for the runtime. The runtime can choose to do anything in this case! It can choose to give a type load error. It can give a verifiability error. It can choose to allow the program but fill in the slots according to some criterion of its own choosing. And in fact the latter is what it does. The runtime takes a look at the program emitted by the C# compiler and decides on its own that possibility one is the correct way to analyze this program.
So, now we have the rather philosophical question of whether or not this is a compiler bug; the compiler is following a reasonable interpretation of the specification, and yet we still do not get the behaviour we expect. In that sense, it very much is a compiler bug. The job of the compiler is to translate a program written in C# into an exactly equivalent program written in IL. The compiler is failing to do so; it is translating a program written in C# into a program written in IL that has implementation-defined behavior, not the behaviour specified by the C# language specification.
As Sam clearly describes in his blog post, we are well aware of this mismatch between what type topologies the C# language endows with specific meanings and what topologies the CLR endows with specific meanings. The C# language is reasonably clear that possibility two is arguably the correct one, but there is no code we can emit that makes the CLR do that because the CLR fundamentally has implementation-defined behaviour any time two methods unify to have the same signature. Our choices are therefore:
The last choice is extremely expensive. Paying that cost buys us a vanishingly small user benefit, and directly takes budget away from solving realistic problems faced by users writing sensible programs. And in any event, the decision to do that is entirely out of my hands.
We on the C# compiler team have therefore chosen to take a combination of the first and third strategies; sometimes we produce warnings or errors for such situations, and sometimes we do nothing and allow the program to do something strange at runtime.
Since in practice these sorts of programs very rarely arise in realistic line-of-business programming scenarios, I don’t feel very bad about these corner cases. If they were cheap and easy to fix then we would fix them, but they’re neither cheap nor easy to fix.
If this subject interests you, see my article on yet another way in which causing two methods to unify leads to a warning and implementation-defined behaviour:
http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx
http://blogs.msdn.com/b/ericlippert/archive/2006/04/06/odious-ambiguous-overloads-part-two.aspx