Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# method overload resolution issues in Visual Studio 2013

Tags:

c#

rx.net

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?

like image 327
Mooh Avatar asked Apr 20 '18 09:04

Mooh


People also ask

What C is used for?

C programming language is a machine-independent programming language that is mainly used to create many types of applications and operating systems such as Windows, and other complicated programs such as the Oracle database, Git, Python interpreter, and games and is considered a programming foundation in the process of ...

What is the full name of C?

In the real sense it has no meaning or full form. It was developed by Dennis Ritchie and Ken Thompson at AT&T bell Lab. First, they used to call it as B language then later they made some improvement into it and renamed it as C and its superscript as C++ which was invented by Dr. Stroustroupe.

Is C language easy?

C is a general-purpose language that most programmers learn before moving on to more complex languages. From Unix and Windows to Tic Tac Toe and Photoshop, several of the most commonly used applications today have been built on C. It is easy to learn because: A simple syntax with only 32 keywords.

What is C language?

C is an imperative procedural language supporting structured programming, lexical variable scope, and recursion, with a static type system. It was designed to be compiled to provide low-level access to memory and language constructs that map efficiently to machine instructions, all with minimal runtime support.


1 Answers

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.

like image 103
Jon Skeet Avatar answered Sep 28 '22 21:09

Jon Skeet