8

以下对重载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>>)'

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

对我来说似乎很清楚,目的是调用第一个重载,方法组参数解析为Tuple.Create<char>(char). 第二个重载不应该应用,因为没有一个 Tuple.Create重载可以转换为预期的Func<char,int,Tuple<char>>类型。我编译器被 混淆了Tuple.Create<char, int>(char, int),但它的返回类型是错误的:它返回一个二元组,因此不能转换为相关Func类型。

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

  1. 为方法组参数指定类型参数:(Tuple.Create<char>也许这实际上是一个类型推断问题?)。
  2. 使参数成为 lambda 表达式而不是方法组:x => Tuple.Create(x)Select(在通话中使用类型推断效果很好)。

不出所料,尝试以Select这种方式调用另一个重载也会失败:

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

这里的确切问题是什么?

4

2 回答 2

20

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

为什么 Func<T> 与 Func<IEnumerable<T>> 有歧义?

这里的确切问题是什么?

托马斯的猜测基本上是正确的。这是确切的细节。

让我们一步一步来。我们有一个调用:

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

重载解析必须确定调用 Select 的含义。字符串或字符串的任何基类都没有方法“Select”,所以这必须是扩展方法。

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

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

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

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

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

方法组何时可以转换为给定的委托类型? 当给定与委托的形式参数类型相同类型的参数时,方法组可转换为委托类型

也就是说,如果给定类型为“A”的表达式“someA”,则 M 可转换为Func<A, R>对表单调用的重载解析是否成功。M(someA)

调用重载解决方案是否成功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 转换要长得多。如果同时添加它们,我想它们的规则会保持一致。

于 2011-03-09T22:40:05.650 回答
5

我猜编译器被 混淆了Tuple.Create<char, int>(char, int),但它的返回类型是错误的:它返回一个二元组。

返回类型不是方法签名的一部分,因此在重载决策期间不考虑它;仅在选择过载后才对其进行验证。所以就编译器所知,Tuple.Create<char, int>(char, int)是一个有效的候选者,它既不比 好也不差Tuple.Create<char>(char),所以编译器无法决定。

于 2011-03-05T13:49:56.017 回答