"Two-level" 具有委托的泛型方法参数推断

"Two-level" generic method argument inference with delegate

考虑以下示例:

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

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

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

这编译得很好。

我想知道为什么我不能删除 <string> 通用参数?我得到一个错误,它不能从用法中推断出来。

我知道这样的推断对编译器来说可能具有挑战性,但它似乎是可行的。

我想要对此行为的解释。

编辑 回答 Jon Hanna 的回答:

那为什么会这样呢?

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);
    }
}

这里我只绑定了一个参数T1 a,但是T2好像也同样难。

您希望编译器从 Fun2 推断出 string,这对 C# 编译器来说要求过高。这是因为它在 Test.

中引用时将 Fun2 视为方法组,而不是委托

如果您更改代码以传入 Fun2 所需的参数并从 Fun 调用它,那么就不需要了,因为您现在有一个 string 参数,允许推断类型:

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, "");
    }
}

为了回答您编辑过的问题,通过提供 T1 类型,您现在已经通过 Fun(0, Fun2); 调用向编译器提供了额外信息。它现在知道它需要一个来自 Fun2 方法组的方法,该方法具有 T1 参数,在本例中为 int。这将它缩小到一种方法,因此它可以推断使用哪种方法。`

无法推断类型,因为类型未在此处定义。

Fun2 不是 Func<string, string>,它虽然可以分配给 Func<string, string>

所以如果你使用:

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

或者:

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

然后您从 Fun2 显式创建 Func<string, string>,并且通用类型推断会相应地工作。

相反,当你这样做时:

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

那么一组重载 Fun<string> 只包含一个接受 Func<string, string> 的重载,编译器可以推断出您想要这样使用 Fun2

但是你要求它根据参数类型推断泛型类型,并根据泛型类型推断参数类型。这是一个比它可以做的任何一种类型的推理都要大的问题。

(值得考虑的是,在 .NET 1.0 中,不仅委托不是通用的——所以你必须定义 delgate string MyDelegate(string test)——而且还需要使用构造函数创建对象 Fun(new MyDelegate(Fun2)). 语法已经改变,使委托的使用在几个方面更容易,但 Fun2 作为 Func<string, string> 的隐式使用仍然是幕后委托对象的构造。

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);
    }
}

因为然后它可以推断,顺序是:

  1. T1int.
  2. Fun2 被分配给 Func<int, T2> 一些 T2
  3. 如果 T2string
  4. Fun2 可以分配给 Func<int, T2>。因此 T2 是字符串。

特别是,Func 的 return 类型一旦你有了参数类型,就可以从函数中推断出来。这也很好(并且值得编译器方面的努力),因为它在 Linq 的 Select 中很重要。这提出了一个相关的案例,事实是只有 x.Select(i => i.ToString()) 我们没有足够的信息来知道 lambda 被转换成什么。一旦我们知道 xIEnumerable<T> 还是 IQueryable<T> 我们就知道我们有 Func<T, ?>Expression<Func<T, ?>> 并且可以从那里推断出其余的。

这里还值得注意的是,推导 return 类型不会像推导其他类型那样存在歧义。考虑一下我们是否在同一个 class 中同时拥有您的 Fun2(接受 string 的那个和接受 int 的那个)。这是有效的 C# 重载,但是推导了 Func<T, string> 的类型,Fun2 可以强制转换为不可能;两者都有效。

然而,虽然 .NET 允许重载 return 类型,但 C# 不允许。因此,一旦确定了 T 的类型,任何有效的 C# 程序都不能对从方法(或 lambda)创建的 Func<T, TResult> 的 return 类型产生歧义。相对简单,结合强大的实用性,编译器可以很好地为我们推断。