This is a fun one :) There are multiple aspects to it. To start with, let's simplify it very significantly by removing Rx and actual overload resolution from the picture. Overload resolution is handled at the very end of the answer.
Anonymous function to delegate conversions, and reachability
The difference here is whether the end-point of the lambda expression is reachable. If it is, then that lambda expression doesn't return anything, and the lambda expression can only be converted to a Func<Task>
. If the end-point of the lambda expression isn't reachable, then it can be converted to any Func<Task<T>>
.
The form of the while
statement makes a difference because of this part of the C# specification. (This is from the ECMA C# 5 standard; other versions may have slightly different wording for the same concept.)
The end point of a while
statement is reachable if at least one of the following is true:
- The
while
statement contains a reachable break statement that exits the while statement.
- The
while
statement is reachable and the Boolean expression does not have the constant value true
.
When you have a while (true)
loop with no break
statements, neither bullet is true, so the end point of the while
statement (and therefore the lambda expression in your case) is not reachable.
Here's a short but complete example without any Rx involved:
using System;
using System.Threading.Tasks;
public class Test
{
static void Main()
{
// Valid
Func<Task> t1 = async () => { while(true); };
// Valid: end of lambda is unreachable, so it's fine to say
// it'll return an int when it gets to that end point.
Func<Task<int>> t2 = async () => { while(true); };
// Valid
Func<Task> t3 = async () => { while(false); };
// Invalid
Func<Task<int>> t4 = async () => { while(false); };
}
}
We can simplify even further by removing async from the equation. If we have a synchronous parameterless lambda expression with no return statements, that's always convertible to Action
, but it's also convertible to Func<T>
for any T
if the end of the lambda expression isn't reachable. Slight change to the above code:
using System;
public class Test
{
static void Main()
{
// Valid
Action t1 = () => { while(true); };
// Valid: end of lambda is unreachable, so it's fine to say
// it'll return an int when it gets to that end point.
Func<int> t2 = () => { while(true); };
// Valid
Action t3 = () => { while(false); };
// Invalid
Func<int> t4 = () => { while(false); };
}
}
We can look at this in a slightly different way by removing delegates and lambda expressions from the mix. Consider these methods:
void Method1()
{
while (true);
}
// Valid: end point is unreachable
int Method2()
{
while (true);
}
void Method3()
{
while (false);
}
// Invalid: end point is reachable
int Method4()
{
while (false);
}
Although the error method for Method4
is "not all code paths return a value" the way this is detected is "the end of the method is reachable". Now imagine those method bodies are lambda expressions trying to satisfy a delegate with the same signature as the method signature, and we're back to the second example...
Fun with overload resolution
As Panagiotis Kanavos noted, the original error around overload resolution isn't reproducible in Visual Studio 2017. So what's going on? Again, we don't actually need Rx involved to test this. But we can see some very odd behavior. Consider this:
using System;
using System.Threading.Tasks;
class Program
{
static void Foo(Func<Task> func) => Console.WriteLine("Foo1");
static void Foo(Func<Task<int>> func) => Console.WriteLine("Foo2");
static void Bar(Action action) => Console.WriteLine("Bar1");
static void Bar(Func<int> action) => Console.WriteLine("Bar2");
static void Main(string[] args)
{
Foo(async () => { while (true); });
Bar(() => { while (true) ; });
}
}
That issues a warning (no await operators) but it compiles with the C# 7 compiler. The output surprised me:
Foo1
Bar2
So the resolution for Foo
is determining that the conversion to Func<Task>
is better than the conversion to Func<Task<int>>
, whereas the resolution for Bar
is determining that the conversion to Func<int>
is better than the conversion to Action
. All the conversions are valid - if you comment out the Foo1
and Bar2
methods, it still compiles, but gives output of Foo2
, Bar1
.
With the C# 5 compiler, the Foo
call is ambiguous by the Bar
call resolves to Bar2
, just like with the C# 7 compiler.
With a bit more research, the synchronous form is specified in 12.6.4.4 of the ECMA C# 5 specification:
C1 is a better conversion than C2 if at least one of the following holds:
- ...
- E is an anonymous function, T1 is either a delegate type D1 or an expression tree type Expression, T2 is either a delegate type D2 or an expression tree type Expression and one of the following holds:
- D1 is a better conversion target than D2 (irrelevant for us)
- D1 and D2 have identical parameter lists, and one of the following holds:
- D1 has a return type Y1, and D2 has a return type Y2, an inferred return type X exists for E in the context of that parameter list (§12.6.3.13), and the conversion from X to Y1 is better than the conversion from X to Y2
- E is async, D1 has a return type
Task<Y1>
, and D2 has a return type Task<Y2>
, an inferred return type Task<X>
exists for E in the context of that parameter list (§12.6.3.13), and the conversion from X to Y1 is better than the conversion from X to Y2
- D1 has a return type Y, and D2 is void returning
So that makes sense for the non-async case - and it also makes sense for how the C# 5 compiler isn't able to resolve the ambiguity, because those rules don't break the tie.
We don't have a full C# 6 or C# 7 specification yet, but there's a draft one available. Its overload resolution rules are expressed somewhat differently, and the change may be there somewhere.
If it's going to compile to anything though, I'd expect the Foo
overload accepting a Func<Task<int>>
to be chosen over the overload accepting Func<Task>
- because it's a more specific type. (There's a reference conversion from Func<Task<int>>
to Func<Task>
, but not vice versa.)
Note that the inferred return type of the lambda expression would just be Func<Task>
in both the C# 5 and draft C# 6 specifications.
Ultimately, overload resolution and type inference are really hard bits of the specification. This answer explains why the while(true)
loop makes a difference (because without it, the overload accepting a func returning a Task<T>
isn't even applicable) but I've reached the end of what I can work out about the choice the C# 7 compiler makes.
while(true)
, the compiler is faced with a method that it knows never returns. I think that's messing up the part of the compiler that is trying to determine the return type of the method. – LarcenyFunc<IObserver<int>, CancellationToken, Task<Action>>
is still valid... – Expediential