C# method overload resolution issues in Visual Studio 2013
Asked Answered
R

2

31

Having these three methods available in Rx.NET library

public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task> subscribeAsync) {...}
public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task<IDisposable>> subscribeAsync) {...}
public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task<Action>> subscribeAsync) {...}

I write the following sample code in MSVS 2013:

var sequence =
  Observable.Create<int>( async ( observer, token ) =>
                          {
                            while ( true )
                            {
                              token.ThrowIfCancellationRequested();
                              await Task.Delay( 100, token );
                              observer.OnNext( 0 );
                            }
                          } );

This does not compile due to ambiguous overloads. Exact output from the compiler being:

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

However as soon as I replace while( true ) with while( false ) or with var condition = true; while( condition )...

var sequence =
  Observable.Create<int>( async ( observer, token ) =>
                          {                            
                            while ( false ) // It's the only difference
                            {
                              token.ThrowIfCancellationRequested();
                              await Task.Delay( 100, token );
                              observer.OnNext( 0 );
                            }
                          } );

the error disappears and method call resolves to this:

public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task> subscribeAsync) {...}

What is going on there?

Retail answered 20/4, 2018 at 9:52 Comment(10)
When it's 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.Larceny
The compiler is too smart. Since you have a while loop that is always false the compiler isn't giving you an error. The error message 1 is showing an unequal number of opening and closing angle bracket.Beggarly
@jdweng: It's not "a while loop that's always false" that's the special case here. For the purposes of this question, that's equivalent to "a while loop where the condition is sometimes false and sometimes true". It's "a while loop that's always true, without any break statements" that's the special case here, making the end of the while loop unreachable.Expediential
@Retail the real issue is that you are using a very old version of Visual Studio. No repro on Visual Studio 2017 and C# 7 or even C# 5. The Roslyn compiler in VS 2015 fixed a lot of similar issues. You can use Visual Studio 2015 or 2017 (Community edition is free btw and equivalent to Professional in features) with VS 2012 projects.Seemly
@PanagiotisKanavos: I'm investigating why that doesn't reproduce in VS2017... I wonder whether it's C# 6+ overload resolution rules.Expediential
@PanagiotisKanavos: The conversion to Func<IObserver<int>, CancellationToken, Task<Action>> is still valid...Expediential
@DaisyShipton I tried it with C# 5 too. It's a different compiler- VS 2015 and later use RoslynSeemly
@PanagiotisKanavos: But VS2015 was C# 6. How did you try it with C# 5 and Roslyn? I still don't think there's an actual compiler bug here - just different overload resolution rules. I'm still investigating.Expediential
@DaisyShipton you can switch the language in VS 2017 from the Build tab. I installed 15.7.3 though, which has additional resolution fixes around method groups. In this case it's clear the correct overload is the one accepting an action. If the compilre gets confused it's clearly a bug.Seemly
@PanagiotisKanavos: Switching language version there doesn't tell you what that version of the language would do. It doesn't change how overload resolution works, in my experience. When you say it's "clear" what the correct overload resolution should be, have you found where it says that in the spec? Again: there are conversions from the lambda expression to all the relevant delegate types, so which tie-break rule are you considering? Have a look at the bottom of my answer for a potential surprise. (I'm still investigating.)Expediential
E
32

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.

Expediential answered 20/4, 2018 at 10:4 Comment(4)
I can't reproduce the problem in Visual Studio 2017. This is an artifact of the pre-Roslyn compiler, not the language itselfSeemly
@PanagiotisKanavos: Roslyn does not always comply with the spec as written either, so that observation alone does not establish which compiler is "at fault", or whether the specs need adjusting. (I'm not even touching the fact that the spec is now perennially out of date as the compiler is moving forward faster than it can be adjusted...)Horologist
"So the resolution for Foo is determining that the conversion to Func<Task<int>>is better than the conversion to Func<Task>, whereas the resolution for Bar is determining that the conversion to Action is better than the conversion to Func<int>." Though I believe it should be like this (no ide at hand to test), your code/output shows the exact opposite...Am I missing something or just confused. Foo1 is the Func<Task> version...Gnathous
@RenéVogt: You're not missing anything - I got confused when typing it up. Will fix.Expediential
D
5

In addition to the answer from @Daisy Shipton I'd like to add that the same behavior can be observed in the following case, too:

var sequence = Observable.Create<int>(
    async (observer, token) =>
    {
        throw new NotImplementedException();
    });

basically because of the same reason - the compiler sees that the lambda function never returns so any return type would match, which in turn makes the lambda match any of the Observable.Create overloads.

And, finally, an example of simple solution: you can cast the lambda to the desired signature type to hint the compiler which Rx overload to choose.

var sequence =
    Observable.Create<int>(
        (Func<IObserver<int>, CancellationToken, Task>)(async (observer, token) =>
        {
            throw new NotImplementedException();
        })
      );
Drachm answered 20/4, 2018 at 10:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.