首先,我注意到这是以下内容的副本:
Why is Func<T> ambiguous with Func<IEnumerable<T>>?
这里的确切问题是什么?
Thomas 的猜测基本上是正确的。以下是具体细节。
让我们一步一步来。我们有一个调用:
"test".Select<char, Tuple<char>>(Tuple.Create);
重载解析必须确定调用 Select 的含义。字符串或字符串的任何基类都没有“Select”方法,所以这必须是扩展方法。
候选集有许多可能的扩展方法,因为字符串可以转换为IEnumerable<char>,并且可能在某处有一个using System.Linq;。有许多扩展方法与“选择,泛型二,在使用给定的方法类型参数构造时将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<char>。现在的问题是,第二个参数(方法组“Tuple.Create”)是否可以隐式转换为形参类型Func<char,Tuple<char>> 和Func<char,int, Tuple<char>>。
方法组何时可以转换为给定的委托类型? 当重载解析成功给定与委托的形参类型相同类型的参数时,方法组可转换为委托类型。
也就是说,如果在给定类型为“A”的表达式“someA”的情况下,对M(someA) 形式的调用的重载解析成功,则 M 可转换为 Func<A, R>。
调用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 转换要长得多。如果它们同时添加,我想它们的规则会保持一致。