Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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!
}
like image 363
Jeppe Stig Nielsen Avatar asked Dec 05 '13 11:12

Jeppe Stig Nielsen


People also ask

How are functions selected for overload resolution?

Before overload resolution begins, the functions selected by name lookup and template argument deduction are combined to form the set of candidate functions (the exact criteria depend on the context in which overload resolution takes place, see below).

Can a compiler be used by multiple different architectures?

In this case the compiler can’t be used by multiple different architectures, because the target code produced on different architectures would be different. Hence portability reduces here.

What happens if overload resolution fails at Phase 1?

if overload resolution fails at phase 1, phase 2 is entered, where the candidate functions are all constructors of T and the argument list for the purpose of overload resolution consists of the individual elements of the initializer list. If the initializer list is empty and T has a default constructor, phase 1 is skipped.

Are user-defined conversion functions member of the implied object argument?

The user-defined conversion functions are considered to be members of the implied object argument for the purpose of determining the type of the implicit object parameter .


1 Answers

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.

like image 61
Eric Lippert Avatar answered Sep 20 '22 17:09

Eric Lippert