"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);
}
}
因为然后它可以推断,顺序是:
T1
是 int
.
Fun2
被分配给 Func<int, T2>
一些 T2
。
如果 T2
是 string
,Fun2
可以分配给 Func<int, T2>
。因此 T2
是字符串。
特别是,Func
的 return 类型一旦你有了参数类型,就可以从函数中推断出来。这也很好(并且值得编译器方面的努力),因为它在 Linq 的 Select
中很重要。这提出了一个相关的案例,事实是只有 x.Select(i => i.ToString())
我们没有足够的信息来知道 lambda 被转换成什么。一旦我们知道 x
是 IEnumerable<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 类型产生歧义。相对简单,结合强大的实用性,编译器可以很好地为我们推断。
考虑以下示例:
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);
}
}
因为然后它可以推断,顺序是:
T1
是int
.Fun2
被分配给Func<int, T2>
一些T2
。
如果 Fun2
可以分配给Func<int, T2>
。因此T2
是字符串。
T2
是 string
,特别是,Func
的 return 类型一旦你有了参数类型,就可以从函数中推断出来。这也很好(并且值得编译器方面的努力),因为它在 Linq 的 Select
中很重要。这提出了一个相关的案例,事实是只有 x.Select(i => i.ToString())
我们没有足够的信息来知道 lambda 被转换成什么。一旦我们知道 x
是 IEnumerable<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 类型产生歧义。相对简单,结合强大的实用性,编译器可以很好地为我们推断。