How does adding a break in a while loop resolve ov

2020-06-30 07:13发布

Consider this Reactive Extensions snippet (ignore the practicality of it):

return Observable.Create<string>(async observable =>
{
    while (true)
    {
    }
});

This does not compile with Reactive Extensions 2.2.5 (using NuGet Rx-Main package). It fails with:

Error 1 The call is ambiguous between the following methods or properties: 'System.Reactive.Linq.Observable.Create<string>(System.Func<System.IObserver<string>,System.Threading.Tasks.Task<System.Action>>)' and 'System.Reactive.Linq.Observable.Create<string>(System.Func<System.IObserver<string>,System.Threading.Tasks.Task>)'

However, adding a break anywhere in the while loop fixes the compilation error:

return Observable.Create<string>(async observable =>
{
    while (true)
    {
        break;
    }
});

The problem can be reproduced without Reactive Extensions at all (easier if you want to try it without fiddling with Rx):

class Program
{
    static void Main(string[] args)
    {
        Observable.Create<string>(async blah =>
        {
            while (true)
            {
                Console.WriteLine("foo.");
                break; //Remove this and the compiler will break
            }
        });
    }
}

public class Observable
{
    public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, Task> subscribeAsync)
    {
        throw new Exception("Impl not important.");
    }

    public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, Task<Action>> subscribeAsync)
    {
        throw new Exception("Impl not important.");
    }
}

public interface IObserver<T>
{
}

Ignoring the Reactive Extensions part of it, Why does adding break help the C# compiler resolve the ambiguity? How can this be described with the rules of overload resolution from the C# specification?

I'm using Visual Studio 2013 Update 2 targeting 4.5.1.

标签: c#
2条回答
疯言疯语
2楼-- · 2020-06-30 07:35

It's easiest to just pull out async as well as the lambdas here, as it emphasizes what's going on. Both of these methods are valid and will compile:

public static void Foo()
{
    while (true) { }
}
public static Action Foo()
{
    while (true) { }
}

However, for these two methods:

public static void Foo()
{
    while (true) { break; }
}
public static Action Foo()
{
    while (true) { break; }
}

The first compiles, and the second does not. It has a code path that doesn't return a valid value.

In fact, while(true){} (along with throw new Exception();) is an interesting statement in that it is the valid body of a method with any return type.

Since the infinite loop is a suitable candidate for both overloads, and neither overload is "better", it results in an ambiguity error. The non-infinite loop implementation only has one suitable candidate in overload resolution, so it compiles.

Of course, to bring async back into play, it is actually relevant in one way here. For the async methods they both always return something, whether it's a Task or a Task<T>. The "betterness" algorithms for overload resolution will prefer delegates that return a value over void delegates when there is a lambda that could match either, however in your case the two overload both have delegates that return a value, the fact that for async methods returning a Task instead of a Task<T> is the conceptual equivalent of not returning a value isn't incorporated into that betterness algorithm. Because of this the non-async equivalent wouldn't result in an ambiguity error, even though both overloads are applicable.

Of course it's worth noting that writing a program to determine if an arbitrary block of code will ever complete is a famously unsolvable problem, however, while the compiler cannot correctly evaluate whether every single snippet will complete, it can prove, in certain simple cases such as this one, that the code will in fact never complete. Because of this there are ways of writing code that will clearly (to you and me) never complete, but that the compiler will treat as possibly completing.

查看更多
家丑人穷心不美
3楼-- · 2020-06-30 07:47

Leaving async out of this to start with...

With the break, the end of the lambda expression is reachable, therefore the return type of the lambda has to be void.

Without the break, the end of the lambda expression is unreachable, so any return type would be valid. For example, this is fine:

Func<string> foo = () => { while(true); };

whereas this isn't:

Func<string> foo = () => { while(true) { break; } };

So without the break, the lambda expression would be convertible to any delegate type with a single parameter. With the break, the lambda expression is only convertible to a delegate type with a single parameter and a return type of void.

Add the async part and void becomes void or Task, vs void, Task or Task<T> for any T where previously you could have any return type. For example:

// Valid
Func<Task<string>> foo = async () => { while(true); };
// Invalid (it doesn't actually return a string)
Func<Task<string>> foo = async () => { while(true) { break; } };
// Valid
Func<Task> foo = async () => { while(true) { break; } };
// Valid
Action foo = async () => { while(true) { break; } };
查看更多
登录 后发表回答