一尘不染

重载的方法组参数会混淆重载的解析?

c#

以下对重载 Enumerable.Select 方法的调用:

var itemOnlyOneTuples = "test".Select<char, Tuple<char>>(Tuple.Create);

失败并出现歧义错误(为清楚起见,删除了命名空间):

The call is ambiguous between the following methods or properties: 
'Enumerable.Select<char,Tuple<char>>
           (IEnumerable<char>,Func<char,Tuple<char>>)'
and 
'Enumerable.Select<char,Tuple<char>>
          (IEnumerable<char>, Func<char,int,Tuple<char>>)'

我当然可以理解,为什么 明确指定类型参数会导致模棱两可(这两个重载都适用),但是这样做后我看不到一个。

对我来说似乎很清楚,它的意图是调用第一个重载,其中method-
group参数解析为Tuple.Create<char>(char)。第二个重载应该不适用,因为任何
Tuple.Create重载都不能转换为期望的Func<char,int,Tuple<char>>类型。我
编译器被弄糊涂了Tuple.Create<char, int>(char, int),但是它的返回类型是错误的:它返回一个二元组,因此不能转换为相关的Func类型。

顺便说一句,以下任何一项都会使编译器满意:

  1. 为method-group参数指定类型参数:(Tuple.Create<char>也许这实际上是类型推断问题?)。
  2. 使参数的λ表达式代替的方法的基团的:x => Tuple.Create(x)。(在Select通话中与类型推断配合使用时效果很好)。

毫不奇怪,尝试以Select这种方式调用另一个重载也会失败:

var itemIndexTwoTuples = "test".Select<char, Tuple<char, int>>(Tuple.Create);

这里的确切问题是什么?


阅读 266

收藏
2020-05-19

共1个答案

一尘不染

首先,我注意到这是重复的:

[为什么Func 与Func <IEnumerable <T

不明确?](https://stackoverflow.com/questions/4573011/why-is-funct-ambiguous-
with-funcienumerablet/4574930#4574930)

这里的确切问题是什么?

托马斯的猜测本质上是正确的。这是确切的详细信息。

让我们一次逐步进行一下。我们有一个调用:

"test".Select<char, Tuple<char>>(Tuple.Create);

重载解析必须确定调用Select的含义。在字符串或字符串的任何基类上都没有方法“选择”,因此它必须是扩展方法。

候选集有多种可能的扩展方法,因为字符串可以转换为,IEnumerable<char>并且大概using System.Linq;在某处有一个。有许多扩展方法与模式“ IEnumerable<char>使用给定方法类型实参构造时,将Select
作为选择”的第一个实参作为模式匹配。

特别是,其中两个 候选人 是:

Enumerable.Select<char,Tuple<char>>(IEnumerable<char>,Func<char,Tuple<char>>)
Enumerable.Select<char,Tuple<char>>(IEnumerable<char>,Func<char,int,Tuple<char>>)

现在,我们面临的第一个问题是应聘者是否 适用 ?也就是说,是否存在从每个提供的参数到对应的形式参数类型的隐式转换?

一个好问题。显然,第一个参数是字符串“ receiver”,并且可以隐式转换为IEnumerable<char>。现在的问题是,第二个参数(方法组“
Tuple.Create”)是否可以隐式转换为形式参数类型Func<char,Tuple<char>>Func<char,int, Tuple<char>>

方法组何时可以转换为给定的委托类型? 如果给定的参数类型与委托的形式参数类型相同,则重载解析将成功后,方法组可转换为委托类型

也就是说,Func<A, R>如果M(someA)给定类型为“ A”的表达式“ someA”,则可以将M转换为是否成功完成了形式调用。

调用会成功解决重载Tuple.Create(someChar)吗?是; 重载解决方案将选择Tuple.Create<char>(char)

调用会成功解决重载Tuple.Create(someChar, someInt)吗?是的,应该选择重载分辨率Tuple.Create<char,int>(char, int)

由于在两种情况下重载解析都将成功,所以方法组可转换为两种委托类型。 其中一种方法的返回类型与委托的返回类型不匹配这一事实是无关紧要的。
根据返回类型分析,重载解析不会成功或失败

可以合理地说,根据返回类型分析, 从方法组到委托类型的可转换性
应该成功还是失败,但这并不是指定语言的方式;该语言被指定为使用重载解析作为方法组转换的测试,我认为这是一个合理的选择。

因此,我们有两个适用的候选人。有什么方法可以决定哪个比另一个 更好 ?规范指出,转换为 更具体的类型 更好;如果你有

void M(string s) {}
void M(object o) {}
...
M(null);

然后重载解析会选择字符串版本,因为字符串比对象更具体。这些委托类型之一比另一种更具体吗?不。没有一个比另一个更具体。(这是更好转换规则的简化;实际上有很多决胜局,但在此均不适用。)

因此,没有理由偏爱一个。

再次,可以合理地说,有一个基础,即那些转换之一将产生委托返回类型不匹配错误,而其中之一则不会。同样,尽管如此,指定该语言是为了通过考虑 形式参数类型
之间的关系来推断是否更好,而不是考虑您选择的转换最终是否会导致错误。

由于没有依据可以偏爱一个,所以这是一个歧义错误。

构造类似的歧义错误很容易。例如:

void M(Func<int, int> f){}
void M(Expression<Func<int, int>> ex) {}
...
M(x=>Q(++x));

那是模棱两可的。即使在表达式树中使用++是非法的, 但可转换逻辑也不会考虑lambda主体内部是否存在某些在表达式树中不合法的东西
。转换逻辑仅确保类型签出,并且它们签入。鉴于此,没有理由偏爱M中的一个,所以这是个歧义。

你注意到

"test".Select<char, Tuple<char>>(Tuple.Create<char>);

成功。您现在知道为什么了。重载分辨率必须确定

Tuple.Create<char>(someChar)

要么

Tuple.Create<char>(someChar, someInt)

会成功。由于第一个候选者不适用,第二个候选者不适用,因此第二个候选者不适用且被淘汰,因此不会变得模棱两可。

您还注意到

"test".Select<char, Tuple<char>>(x=>Tuple.Create(x));

是明确的。Lambda转换 确实
考虑了返回表达式的类型与目标委托的返回类型的兼容性。不幸的是,方法组和lambda表达式使用两种微妙的不同算法来确定可转换性,但是现在我们仍然坚持使用它。请记住,方法组转换的语言比lambda转换的语言长很多;如果同时添加它们,我想他们的规则应该是一致的。

2020-05-19