Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Two-level" generic method argument inference with delegate

Consider the following example:

class Test
{
    public void Fun<T>(Func<T, T> f)
    {
    }

    public string Fun2(string test)
    {
        return ""; 
    }

    public Test()
    {
        Fun<string>(Fun2);
    }
}

This compiles well.

I wonder why cannot I remove <string> generic argument? I get an error that it cannot be inferred from the usage.

I understand that such an inference might be challenging for the compiler, but nevertheless it seems possible.

I would like an explanation of this behaviour.

Edit answering Jon Hanna's answer:

Then why this works?

class Test
{
    public void Fun<T1, T2>(T1 a, Func<T1, T2> f)
    {
    }

    public string Fun2(int test)
    {
        return test.ToString(); 
    }

    public Test()
    {
        Fun(0, Fun2);
    }
}

Here I bind only one parameter with T1 a, but T2 seems to be similarly difficult.

like image 915
Piotr Zierhoffer Avatar asked Sep 17 '15 10:09

Piotr Zierhoffer


2 Answers

It can't infer the type, because the type isn't defined here.

Fun2 is not a Func<string, string>, it is though something that can be assigned to Func<string, string>.

So if you use:

public Test()
{
  Func<string, string> del = Fun2;
  Fun(del);
}

Or:

public Test()
{
  Fun((Func<string, string>)Fun2);
}

Then you are explicitly creating a Func<string, string> from Fun2, and generic type-inference works accordingly.

Conversely, when you do:

public Test()
{
  Fun<string>(Fun2);
}

Then the set of overloads Fun<string> contains only one that accepts a Func<string, string> and the compiler can infer that you want to use Fun2 as such.

But you're asking it to infer both the generic type based on the the type of the argument, and the type of the argument based on the generic type. That's a bigger ask than either of the types of inference it can do.

(It's worth considering that in .NET 1.0 not only were delegates not generic—so you would have had to define delgate string MyDelegate(string test)—but it was also necessary to create the object with a constructor Fun(new MyDelegate(Fun2)). The syntax has changed to make use of delegates easier in several ways, but the implicit use of Fun2 as Func<string, string> is still a construction of a delegate object behinds the scenes).

Then why this works?

class Test
{
    public void Fun<T1, T2>(T1 a, Func<T1, T2> f)
    {
    }

    public string Fun2(int test)
    {
        return test.ToString(); 
    }

    public Test()
    {
        Fun(0, Fun2);
    }
}

Because then it can infer, in order:

  1. T1 is int.
  2. Fun2 is being assigned to Func<int, T2> for some T2.
  3. Fun2 can be assigned to a Func<int, T2> if T2 is string. Therefore T2 is string.

In particular, the return type of Func can be inferred from a function once you have the argument types. This is just as well (and worth the effort on the part of the compiler) because it's important in Linq's Select. That brings up a related case, the fact that with just x.Select(i => i.ToString()) we don't have enough information to know what the lambda gets cast to. Once we know whether the x is IEnumerable<T> or IQueryable<T> we know we either have Func<T, ?> or Expression<Func<T, ?>> and the rest can be inferred from there.

It's also worth noting here, that deducing the return type is not subject to an ambiguity that deducing the other types are. Consider if we had both your Fun2 (the one that takes a string and the one that takes an int) in the same class. This is valid C# overloading, but makes deduction of the type of Func<T, string> that Fun2 can be cast to impossible; both are valid.

However, while .NET does allow overloading on return type, C# does not. So no valid C# program can be ambiguous on the return type of a Func<T, TResult> created from a method (or lambda) once the type of T is determined. That relative ease, combined with the great usefulness, makes it something the compiler is well to infer for us.

like image 198
Jon Hanna Avatar answered Oct 17 '22 13:10

Jon Hanna


You are wanting the compiler to infer string from Fun2, which is too much of an ask for the C# compiler. This is because it sees Fun2 as a method group, not a delegate, when referenced in Test.

If you change the code to pass in the parameter needed by Fun2 and invoke it from Fun, then the need goes away, as you now have a string parameter, allowing the type to be inferred:

class Test
{
    public void Fun<T>(Func<T, T> f, T x)
    {
        f(x);
    }

    public string Fun2(string test)
    {
        return test;
    }

    public Test()
    {
        Fun(Fun2, "");
    }
}

To answer your edited version of the question, by supplying the T1 type you have now supplied the compiler - via the Fun(0, Fun2); call - extra information. It now knows it needs a method, from the Fun2 method group, that has a T1 parameter, in this case int. This narrows it down to one method and so it can infer which one to use.`

like image 26
David Arno Avatar answered Oct 17 '22 13:10

David Arno