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.
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:However, for these two methods:
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 withthrow 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 theasync
methods they both always return something, whether it's aTask
or aTask<T>
. The "betterness" algorithms for overload resolution will prefer delegates that return a value overvoid
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 forasync
methods returning aTask
instead of aTask<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.
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:
whereas this isn't:
So without the
break
, the lambda expression would be convertible to any delegate type with a single parameter. With thebreak
, the lambda expression is only convertible to a delegate type with a single parameter and a return type ofvoid
.Add the
async
part andvoid
becomesvoid
orTask
, vsvoid
,Task
orTask<T>
for anyT
where previously you could have any return type. For example: