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

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

这里的确切问题是什么?
2个回答

20

首先,我注意到这是一个重复的问题:

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

这里的确切问题是什么?

Thomas的猜测基本上是正确的。以下是确切的细节。

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

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

重载决议需要确定对 Select 的调用的含义。String 类或其任何基类中没有名为“Select”的方法,因此这必须是一个扩展方法。

由于字符串可转换为 IEnumerable<char>,并且可能在其中有一个 using System.Linq;,所以候选集中有许多匹配模式“Select,泛型度量为二,使用给定方法类型参数构造时,以 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>>) 
现在,我们面临的第一个问题是候选项是否“适用”?也就是说,每个提供的参数是否有从相应的形式参数类型进行隐式转换的方式?
一个很好的问题。显然,第一个参数将是“接收者”,即一个字符串,并且它将隐式转换为IEnumerable 。现在的问题是第二个参数——方法组“Tuple.Create”是否可以隐式转换为形式参数类型Func >和Func >。
何时可以将方法组转换为给定的委托类型?当重载分辨率使用与委托的形式参数类型相同的类型的参数成功时,方法组可转换为委托类型。
也就是说,如果以某种形式调用M(someA)的重载分辨率会成功,则M可转换为Func ,其中'someA'是类型'A'的表达式。
对于调用Tuple.Create(someChar)的情况,重载分辨率是否成功?是的;重载分辨率将选择Tuple.Create (char)。
对于调用Tuple.Create(someChar,someInt)的情况,重载分辨率是否成功?是的,重载分辨率将选择Tuple.Create (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 而不是另一个 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转换更久了;如果它们在同一时间添加,我想他们的规则会被制定得更加一致。


4
Eric说“我们现在只能使用这两种略有不同的算法”暗示着这里考虑到了向后兼容性的问题,对吗?假定让方法组转换使用返回类型分析来解决会导致现有代码选择不同的重载,但是我无法构建一个实际发生这种情况的场景 - 实际上是否存在这样的情况?或者这更多是为了平衡风险/回报而针对低价值用例的问题? - LBushkin
我非常感激这个答案中的细节; 我现在对此有了稍微更好的理解。实际上,我原本以为我的代码会工作,因为我认为C# 4中方法组的返回类型会得到更多的“关注”,但这可能只适用于类型推断?总之,对于我来说,重载决议和类型推断有一些黑魔法的成分; 我的直觉经常让我失望。 - Ani
另外,我发现有点困惑的是,C# 倾向于强烈地分离以下两个方面:a)用户可能意味着什么,b)用户可能意味着的东西是否合法。希望在 a) 中更加重视 b) 的作用,这样做是否有错呢? - Ani
@Ani:现在方法组的返回类型在某些情况下被用于方法类型推断中。例如,如果你有这样的方法M<T, U>(T t, Func<T, U>)和int N(string),然后你调用M("hello", N),那么首先我们会推断T是string,接着我们对N(someString)进行重载决议并成功匹配,再推断U是int。 - Eric Lippert

5
我猜编译器被 Tuple.Create<char, int>(char, int) 搞糊涂了,但它的返回类型是错误的:它返回一个二元组。返回类型不是方法签名的一部分,因此在重载决策期间不会考虑它;只有在选择重载后才会验证它。所以就编译器而言,Tuple.Create<char, int>(char, int) 是一个有效的候选项,既不比 Tuple.Create<char>(char) 更好也不更差,因此编译器无法决定。

谢谢,这听起来很有道理。你有任何参考资料可以证实吗? - Ani
这不仅是可能的,而且是准确的。在SO上引用很困难,投票是匿名的。 - Hans Passant
@Thomas:这个问题描述得有点模糊。你能更清楚地解释一下吗?请注意,Tuple.Create 的所有重载都不能转换为 Func<char,int,Tuple<char>>,因此仅从句子“编译器只检查提供的参数是否与声明的参数匹配”来看,似乎第二个 Select 重载不再是可适用的函数成员。显然,我刚才说的是错误的,但是你的回答似乎没有解决它(以我理解的方式)。 - Ani
@Ani:实际上,在您的情况下涉及到两个重载解析:一个是解析正确的Tuple.Create重载,另一个是解析正确的Enumerable.Select重载。当然,第二个的结果取决于第一个的结果。抱歉,我不太擅长解释规范(我自己也不太理解)...希望Jon Skeet或Eric Lippert能看到您的问题并回答它 ;) - Thomas Levesque
@Thomas:你说的两个分辨率是正确的(我不知道将方法组转换为委托时找到最佳适用重载的过程是否也称为“重载分辨率”)。无论如何,感谢你的回答。我不是要求非常精确的描述,但可能有更准确的方式来描述这个问题。 - Ani
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接