16

Understanding the C# Language Specification on overload resolution is clearly hard, and now I am wondering why this simple case fails:

void Method(Func<string> f)
{
}
void Method(Func<object> f)
{
}
void Call()
{
    Method(() => { throw new NotSupportedException(); });
}

This gives compile-time error CS0121, The call is ambiguous between the following methods or properties: followed by my two Method function members (overloads).

What I would have expected was that Func<string> was a better conversion target than Func<object>, and then the first overload should be used.

Since .NET 4 and C# 4 (2010), the generic delegate type Func<out TResult> has been covariant in TResult, and for that reason an implicit conversion exists from Func<string> to Func<object> while clearly no implicit conversion can exist from Func<object> to Func<string>. So it would make Func<string> the better conversion target, and the overload resolution should pick the first overload?

My question is simply: What part of the C# Spec am I missing here?


Addition: This works fine:

void Call()
{
    Method(null); // OK!
}
Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • PS! This works fine: `void Test(Func f1, Func f2, bool b) { var x = b ? f1 : f2; }` – Jeppe Stig Nielsen Dec 05 '13 at 11:24
  • I can't nail it down, but it may be related to this post: http://blogs.msdn.com/b/ericlippert/archive/2011/02/21/never-say-never-part-one.aspx There, Eric explains that functions never reach their endpoint or any return actually does not have any return value at all. So I assume the compiler would choose the first method over the second, if your lambda would be of type Func. But its of "unknown type". somehow.. erm.. ;) – Imi Dec 05 '13 at 11:38
  • @Imi A lambda by itself has no delegate (or expression tree) type. For my lambda, also the return type cannot be found. But my lambda is surely implicitly convertible to `Func`. My lambda is also implicitly convertible to `Func`. So both overloads are "applicable". Now, is one "better" than the other? Yes, because `Func` is a better conversion target than `Func`. Also see my addition to the question (the literal `null` in itself has no type, but `null` is implicitly convertible to both `Func` and `Func`). – Jeppe Stig Nielsen Dec 05 '13 at 12:13
  • 1
    The fact that it picks correctly for `null` suggests that *7.5.3.5 Better conversion target* is being adhered to correctly. Maybe there's an issue in *7.5.3.3 Better conversion from expression* or even before that. I was going to post an answer going through my reading of the spec but I think it's sufficient for me to say that I think you're right and that this should work. – Rawling Dec 05 '13 at 12:21
  • Feel like there's a similar question has been asked, but cannot recall .. – Ken Kin Dec 10 '13 at 19:52

2 Answers2

17

My question is simply: What part of the C# Spec am I missing here?

Summary:

  • You have found a minor known bug in the implementation.
  • The bug will be preserved for backwards compatibility reasons.
  • The C# 3 specification contained an error regarding how the "null" case was to be handled; it was fixed in the C# 4 specification.
  • You can reproduce the buggy behavior with any lambda where the return type cannot be inferred. For example: Method(() => null);

Details:

The C# 5 specification says that the betterness rule is:

  • If the expression has a type then choose the better conversion from that type to the candidate parameter types.

  • If the expression does not have a type and is not a lambda, choose the conversion to the type that is better.

  • If the expression is a lambda then first consider which parameter type is better; if neither is better and the delegate types have identical parameter lists then consider the relationship between the inferred return type of the lambda and the return types of the delegates.

So the intended behaviour is: first the compiler should check to see if one parameter type is clearly better than the other, regardless of whether the argument has a type. If that doesn't resolve the situation and the argument is a lambda, then check to see which of the inferred return type converted to the parameters' delegate types' return type is better.

The bug in the implementation is the implementation doesn't do that. Rather, in the case where the argument is a lambda it skips the type betterness check entirely and goes straight to the inferred return type betterness check, which then fails because there is no inferred return type.

My intention was to fix this for Roslyn. However, when I went to implement this, we discovered that making the fix caused some real-world code to stop compiling. (I do not recall what the real-world code was and I no longer have access to the database that holds the compatibility issues.) We therefore decided to maintain the existing small bug.

I note that the bug was basically impossible before I added delegate variance in C# 4; in C# 3 it was impossible for two different delegate types to be more or less specific, so the only rule that could apply was the lambda rule. Since there was no test in C# 3 that would reveal the bug, it was easy to write. My bad, sorry.

I note also that when you start throwing expression tree types into the mix, the analysis gets even more complicated. Even though Func<string> is better than Func<object>, Expression<Func<string>> is not convertible to Expression<Func<object>>! It would be nice if the algorithm for betterness was agnostic with respect to whether the lambda was going to an expression tree or a delegate, but it is in some ways not. Those cases get complicated and I don't want to labour the point here.

This minor bug is an object lesson in the importance of implementing what the spec actually says and not what you think it says. Had I been more careful in C# 3 to ensure that the code matched the spec then the code would have failed on the "null" case and it would then have been clear earlier that the C# 3 spec was wrong. And the implementation does the lambda check before the type check, which was a time bomb waiting to go off when C# 4 rolled around and suddenly that became incorrect code. The type check should have been done first regardless.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 2
    Hi Eric, thanks for the answer! I've just tried compiling this using Roslyn and you end up with the same ambiguous call message in the diagnostics, so I guess it didn't make it in. – Rawling Dec 06 '13 at 08:38
  • Another case where the lambda has no return type is `Method(() => null);` which suffers from the same bug (as expected from your answer). – Jeppe Stig Nielsen Dec 06 '13 at 08:48
  • What version of the spec are we referring to? With the version that comes with VS2013 (is in `C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC#\Specifications\1033` on my machine), section _7.5.3.3 Better conversion from expression_, the second bullet says _"• E is not an anonymous function and T1 is a better conversion target than T2 (§7.5.3.5)"_. The `null` literal seems to go here, so it seems that there is no bug in the spec regarding `Method(null)`. – Jeppe Stig Nielsen Dec 06 '13 at 08:58
  • 2
    @JeppeStigNielsen: Ah good, it has been fixed in the spec. I am not at my work machine and I have an older copy of the spec here. Previous versions of the spec did not have that language. – Eric Lippert Dec 06 '13 at 14:33
  • Will you please check when you get access to a new Spec, and update your answer? I am not sure I understand this as well as you, but in the bullet i quoted, _"• E is not an anonymous function and T1 is a better conversion target than T2"_, what would happen if one removed the "E is not an anonymous function" part? (Since concrete delegate types are always sealed, I guess only co- and/or contravariance can lead to one type being a better target than another.) – Jeppe Stig Nielsen Dec 06 '13 at 15:56
  • @JeppeStigNielsen: I've reviewed the changes to the specifications and reviewed my notes, and updated the answer accordingly. – Eric Lippert Dec 06 '13 at 16:35
  • An example of code that would fail to compile if this is fixed is `using System; class B {} class D : B {} public class C { static void F(B a, Func f) {} static void F(D a, Func f) {} public static void M(object obj) { F(new D(), n => 0); } }` because `Func` is implicitly convertible to `Func`, and `D` is implicitly convertible to `B`. See https://github.com/icsharpcode/NRefactory/pull/297 – erikkallen Dec 29 '13 at 11:12
4

Well, you are right. What causes problem here is the delegate you are passing as an argument. It has no explicit return type, you are just throwing an exception. Exception is basically an object but it is not considered as a return type of a method. Since there is no return call following the exception throw, compiler is not sure what overload it should use.

Just try this

void Call()
{
    Method(() => 
    { 
        throw new NotSupportedException();
        return "";
    });
}

No problem with choosing an overload now because of explicitly stated type of an object passed to a return call. It does not matter that the return call is unreachable due to the exception throw, but now the compiler knows what overload it should use.

EDIT:

As for the case with passing null, frenkly, I don't know the answer.

Ondrej Janacek
  • 12,486
  • 14
  • 59
  • 93
  • 3
    The fact the anonymous method throws an exception (or, say, returns null) means that it can be converted to either type of `Func`, yes, and of course giving it a specific return type restricts that. But according to the spec on overload resolution, it still shouldn't be a problem. This isn't answering why the compiler can't tell the difference, only offering a workaround. – Rawling Dec 05 '13 at 12:06
  • I didn't read the spec. But what I know is that during the translation when overload resolution is in question compiler tries to tell the return type of a method by examining return calls and when there is none then a resolution can't be set. – Ondrej Janacek Dec 05 '13 at 12:14
  • @OndrejJanacek Rawling is right, it is not an answer to what I am trying to ask. If you have two methods `void Method(Func f) { } void Method(Action a) { }` then the call `Method(() => { throw new NotSupportedException(); });` is allowed and goes to the overload with `Func`. So just because the lambda in itself is not clear about the return type, you can't say everything must fail. – Jeppe Stig Nielsen Dec 05 '13 at 12:19
  • To edit: When saying `Method(null);`, everything works as expected, the call goes to the better function member, so we don't seek an answer for that case. The addition was meant to illustrate that it works fine (i.e. as expected from the Spec) in related cases. – Jeppe Stig Nielsen Dec 05 '13 at 12:22
  • @JeppeStigNielsen Then I should list through spec and try to figure it out. – Ondrej Janacek Dec 05 '13 at 12:23