Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Call is Ambiguous when passing a method group as delegate

Tags:

c#

Hopefully someone can explain this to me. Sorry if it's a repeat, the keywords to explain what I'm seeing are beyond me for now..

here is some code that compiles

class Program
{
    static void Main(string[] args)
    {
        new Transformer<double, double>(Math.Sqrt);
    }
}

class Transformer<Tin, Tout>
{
    Func<Tin, Task<Tout>> actor;
    public Transformer(Func<Tin, Tout> actor)
    {
        this.actor = input => Task.Run<Tout>(() => actor(input));
    }
}

and here is some code that does not

class Program
{
    static void Main(string[] args)
    {
        new Transformer<double, double>(Math.Sqrt);
    }
}

public class Transformer<Tin, Tout>
{
    Func<Tin, Task<Tout>> actor;
    public Transformer(Func<Tin, Tout> actor)
    {
        this.actor = input => Task.Run<Tout>(() => actor(input));
    }

    public Transformer(Func<Tin, Task<Tout>> actor)
    {
        this.actor = actor;
    }
}

By adding the constructor overload, this apparently creates ambiguity but I'm not sure why. Math.Sqrt is not overloaded and clearly has a return type of double, not Task<double>.

Here is the error:

The call is ambiguous between the following methods or properties: 'ConsoleApplication1.Transformer<double,double>.Transformer(System.Func<double,double>)' and 'ConsoleApplication1.Transformer<double,double>.Transformer(System.Func<double,System.Threading.Tasks.Task<double>>)'

Can someone explain why the choice is not obvious to the compiler?


Easy workaround for those who care:

class Program
{
    static void Main(string[] args)
    {
        new Transformer<double, double>(d => Math.Sqrt(d));
    }
}
like image 218
eisenpony Avatar asked Aug 13 '15 17:08

eisenpony


1 Answers

You have a slight misinterpretation of how Func<Tin, Tout> works. Take a look at the docs:

public delegate TResult Func<in T, out TResult>(
    T arg
)

The first argument is a parameter and the last argument is the return type.

When you look at this simplified version of your code:

internal class Program
{
    public static void Main(string[] args)
    {
        new MyClass<double, double>(Method);
    }

    private static double Method(double d)
    {
        throw new NotImplementedException();
    }
}


internal class MyClass<T, U>
{
    public MyClass(Func<U, T> arg)
    {
    }

    public MyClass(Func<U, Task<T>> arg)
    {
    }
}

You will notice that both arguments first specify the double, which is an argument, and then differ in return type: the T vs Task<T>.

However as we both know: overloading is done based on method name, parameter arity and parameter types. Return types are entirely ignored. In our case that means we have two Func<Tin, Tout> with double as argument and T vs Task<T> as return type.

Switching the arguments around compiles just fine:

internal class MyClass<T, U>
{
    public MyClass(Func<T, U> arg)
    {
    }

    public MyClass(Func<Task<T>, U> arg)
    {
    }
}

If you'll look in Visual Studio you'll notice that this method is now greyed out, which makes sense because the argument to Method is of type double and as such will always match T and not Task<T>.

So in order to test that it would now hit the correct overload if you pass in a different, asynchronous method, you can add in a second method:

private static double MethodAsync(Task<double> d)
{
   throw new NotImplementedException();
}

and call it using

new MyClass<double, double>(MethodAsync);

You will now notice that the asynchronous Func<Task<T>, U>> is hit (which you can verify by doing a simply print to console from the constructors).


In short: you're trying to perform overload resolution on a return type, which obviously isn't possible.

like image 89
Jeroen Vannevel Avatar answered Oct 23 '22 06:10

Jeroen Vannevel