重载的方法组参数会混淆重载决策?

Ani*_*Ani 8 c# method-group overload-resolution c#-4.0

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

var itemOnlyOneTuples = "test".Select<char, Tuple<char>>(Tuple.Create);
Run Code Online (Sandbox Code Playgroud)

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

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>>)'
Run Code Online (Sandbox Code Playgroud)

我当然可以理解为什么明确指定类型参数会导致歧义(两个都会应用重载),但是在这样做后我没有看到.

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

顺便说一句,以下任何一个使编译器开心:

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

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

var itemIndexTwoTuples = "test".Select<char, Tuple<char, int>>(Tuple.Create);
Run Code Online (Sandbox Code Playgroud)

这里的确切问题是什么?

Eri*_*ert 20

首先,我注意到这是一个副本:

为什么Func <T>与Func <IEnumerable <T >>不明确?

这里的确切问题是什么?

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

让我们一步一步地完成它.我们有一个调用:

"test".Select<char, Tuple<char>>(Tuple.Create); 
Run Code Online (Sandbox Code Playgroud)

重载决策必须确定对Select的调用的含义.字符串或字符串的任何基类都没有"Select"方法,因此这必须是扩展方法.

候选集有许多可能的扩展方法,因为字符串可以转换为,IEnumerable<char>并且可能using System.Linq;在某处存在.有许多扩展方法匹配模式"Select,generic arity two,IEnumerable<char>在使用给定的方法类型参数构造时将其作为第一个参数".

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

Enumerable.Select<char,Tuple<char>>(IEnumerable<char>,Func<char,Tuple<char>>)
Enumerable.Select<char,Tuple<char>>(IEnumerable<char>,Func<char,int,Tuple<char>>) 
Run Code Online (Sandbox Code Playgroud)

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

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

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

也就是说,Func<A, R>如果M(someA)表达式'someA'的类型为'A',则表示调用的重载决策是否成功,则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);
Run Code Online (Sandbox Code Playgroud)

然后重载决策选择字符串版本,因为字符串比对象更具体.这些委托类型之一是否比另一个更具体?不.两者都没有比另一个更具体.(这是更好的转换规则的简化;实际上有许多破坏者,但这些都不适用于此.)

因此,没有理由偏爱另一个.

同样,有人可以合理地说,确实存在一个基础,即其中一个转换会产生委托返回类型不匹配错误,其中一个不会.但是,再一次,通过考虑形式参数类型之间的关系,而不是关于您选择的转换是否最终会导致错误,指定语言来推理更好.

由于没有依据优先于其他的基础,这是一个模糊性错误.

构造类似的模糊错误很容易.例如:

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

这是模棱两可的.尽管在表达式树中包含++是非法的,但是可转换逻辑不会考虑lambda的主体是否在其中包含某些在表达式树中非法的内容.转换逻辑只是确保类型检出,而且确实如此.鉴于此,没有理由偏好M中的一个而不是另一个,所以这是一个模糊性.

你注意到了

"test".Select<char, Tuple<char>>(Tuple.Create<char>); 
Run Code Online (Sandbox Code Playgroud)

成功.你现在知道为什么了.重载决议必须确定是否

Tuple.Create<char>(someChar)
Run Code Online (Sandbox Code Playgroud)

要么

Tuple.Create<char>(someChar, someInt)
Run Code Online (Sandbox Code Playgroud)

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

你也注意到了

"test".Select<char, Tuple<char>>(x=>Tuple.Create(x)); 
Run Code Online (Sandbox Code Playgroud)

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

  • Eric,声明*"我们坚持[两种微妙的不同算法]"*对于可兑换性表明在这里有一个向后兼容性考虑,是吗?据推测,允许方法组转换使用返回类型分析来解决问题会产生现有代码会导致不同的重载选择的情况.但是,我无法构建一个实际发生这种情况的场景 - 实际上是否存在这种情况?或者这更像是一个平衡低价值用例的风险/回报的问题? (4认同)

Tho*_*que 5

我猜测编译器很困惑Tuple.Create<char, int>(char, int),但它的返回类型是错误的:它返回一个两元组.

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