Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<blockquote> <p>We believe this example exhibits a bug in the C# compiler. </p> </blockquote> <p>Let's do what we should always do when exhibiting a compiler bug: <strong>carefully contrast the expected and observed behaviours.</strong></p> <p>The observed behaviour is that the program produces 11 and 101 as the first and second outputs, respectively.</p> <p>What is the expected behaviour? There are two "virtual slots". The first output should be the result of calling the method in the <code>Foo(T)</code> slot. The second output should be the result of calling the method in the <code>Foo(S)</code> slot.</p> <p>What goes in those slots?</p> <p>In an instance of <code>Base&lt;T,S&gt;</code> the <code>return 1</code> method goes in the <code>Foo(T)</code> slot, and the <code>return 2</code> method goes in the <code>Foo(S)</code> slot.</p> <p>In an instance of <code>Intermediate&lt;T,S&gt;</code> the <code>return 11</code> method goes in the <code>Foo(T)</code> slot and the <code>return 2</code> method goes in the <code>Foo(S)</code> slot. </p> <p>Hopefully so far you agree with me.</p> <p>In an instance of <code>Conflict</code>, there are four possibilities:</p> <ul> <li>Possibility one: the <code>return 11</code> method goes in the <code>Foo(T)</code> slot and the <code>return 101</code> method goes in the <code>Foo(S)</code> slot. </li> <li>Possibility two: the <code>return 101</code> method goes in the <code>Foo(T)</code> slot and the <code>return 2</code> method goes in the <code>Foo(S)</code> slot. </li> <li>Possibility three: the <code>return 101</code> method goes in both slots.</li> <li>Possibility four: the compiler detects that the program is ambiguous and issues an error.</li> </ul> <p>You expect that one of two things will happen here, based on section 10.6.4 of the specification. Either:</p> <ol> <li>The compiler will determine that the method in <code>Conflict</code> overrides the method in <code>Intermediate&lt;string, string&gt;</code>, because the method in the intermediate class is found first. In this case, possibility two is the correct behaviour. Or:</li> <li>The compiler will determine that the method in <code>Conflict</code> is ambiguous as to which <em>original</em> declaration it overrides, and therefore possibility four is the correct one. </li> </ol> <p>In neither case is possibility one correct.</p> <p>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 <em>overriding method</em> as a <em>private implementation detail</em> of the intermediate class; the relevant question to my mind is not whether the intermediate class <em>overrides</em> a base class method, but rather whether it <em>declares</em> a method with a matching signature. In that case the correct behaviour would be to pick possibility four.</p> <p>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 <em>declared</em> in the intermediate class. The compiler determines that <code>Intermediate&lt;string, string&gt;.Foo</code> is the method overridden by <code>Conflict.Foo</code>, and emits the code accordingly. It does not produce an error because it judges that the program is not in error.</p> <p>So if the compiler is correctly analyzing the code, choosing possibility two, and not producing an error, then why at <em>runtime</em> does it <em>appear</em> that the compiler chose possibility one, not possibility two?</p> <p><strong>Because making a program that causes two methods to unify under generic construction is implementation-defined behaviour for the runtime</strong>. The runtime can choose to do <em>anything</em> 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.</p> <p>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. <strong>The job of the compiler is to translate a program written in C# into an exactly equivalent program written in IL</strong>. 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.</p> <p>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 <em>there is no code we can emit that makes the CLR do that</em> because <em>the CLR fundamentally has implementation-defined behaviour any time two methods unify to have the same signature</em>. Our choices are therefore:</p> <ul> <li>Do nothing. Allow these crazy, unrealistic programs to continue to have behaviour that does not precisely match the C# specification.</li> <li>Use heuristics. As Sam notes, we could be more clever about using metadata mechanisms to tell the CLR what methods override what other methods. But... <strong>those mechanisms use the method signatures to disambiguate ambiguous cases</strong> and now we are back in the same boat as we were before; we are now using a mechanism with implementation-defined behaviour in order to disambiguate a program with implementation-defined behaviour! This is a non-starter.</li> <li>Cause the compiler to produce warnings or errors whenever it might be emitting a program whose behaviour is implementation-defined by the runtime.</li> <li>Fix the CLR so that behaviour of type topologies that cause methods to unify in signature is well-defined and matches that of the C# language.</li> </ul> <p>The last choice is extremely expensive. Paying that cost buys us a vanishingly small user benefit, and directly takes budget away from solving <em>realistic</em> problems faced by users writing sensible programs. And in any event, the decision to do that is entirely out of my hands. </p> <p>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. </p> <p>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.</p> <p>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:</p> <p><a href="http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx" rel="noreferrer">http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx</a></p> <p><a href="http://blogs.msdn.com/b/ericlippert/archive/2006/04/06/odious-ambiguous-overloads-part-two.aspx" rel="noreferrer">http://blogs.msdn.com/b/ericlippert/archive/2006/04/06/odious-ambiguous-overloads-part-two.aspx</a></p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload