Peculiar overload resolution with while (true)

2020-01-27 02:02发布

问题:

I was implementing sync/async overloads when I came across this peculiar situation:

When I have a regular lambda expression without parameters or a return value it goes to the Run overload with the Action parameter, which is predictable. But when that lambda has a while (true) in it it goes to the overload with the Func parameter.

public void Test()
{
    Run(() => { var name = "bar"; });
    Run(() => { while (true) ; });
}

void Run(Action action)
{
    Console.WriteLine("action");
}

void Run(Func<Task> func) // Same behavior with Func<T> of any type. 
{
    Console.WriteLine("func");
}

Output:

action
func

So, how can that be? Is there a reason for it?

回答1:

So to start with, the first expression can only possibly call the first overload. It is not a valid expression for a Func<Task> because there is a code path that returns an invalid value (void instead of Task).

() => while(true) is actually a valid method for either signature. (It, along with implementations such as () => throw new Expression(); are valid bodies of methods that return any possible type, including void, an interesting point of trivia, and why auto generated methods from an IDE typically just throw an exception; it'll compile regardless of the signature of the method.) A method that loops infinitely is a method in which there are no code paths that don't return the correct value (and that's true whether the "correct value" is void, Task, or literally anything else). This is of course because it never returns a value, and it does so in a way that the compiler can prove. (If it did so in a way that the compiler couldn't prove, as it hasn't solved the halting problem after all, then we'd be in the same boat as A.)

So, for our infinite loop, which is better, given that both overload are applicable. This brings us to our betterness section of the C# specs.

If we go to section 7.4.3.3, bullet 4, we see:

If E is an anonymous function, T1 and T2 are delegate types or expression tree types with identical parameter lists, and an inferred return type X exists for E in the context of that parameter list (§7.4.2.11):

[...]

if T1 has a return type Y, and T2 is void returning, then C1 is the better conversion.

So when converting from an anonymous delegate, which is what we're doing, it will prefer the conversion that returns a value over one that is void, so it chooses Func<Task>.