Why can't the compiler tell the better conversion target in this overload resolution case? (covariance)

666 views Asked by At

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!
}
2

There are 2 answers

7
Eric Lippert On BEST ANSWER

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.

5
Ondrej Janacek On

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.