对重载的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
类型。顺便说一句,以下任何一项都会使编译器满意:
为method-group参数指定类型参数:
Tuple.Create<char>
(也许这实际上是类型推断问题?)。使参数成为lambda表达式,而不是方法组:
x => Tuple.Create(x)
。 (与Select
调用中的类型推断配合使用时效果很好)。毫不奇怪,尝试以这种方式调用
Select
的另一个重载也会失败:var itemIndexTwoTuples = "test".Select<char, Tuple<char, int>>(Tuple.Create);
这里的确切问题是什么?
最佳答案
首先,我注意到这是重复的:
Why is Func<T> ambiguous with Func<IEnumerable<T>>?
这里的确切问题是什么?
托马斯的猜测本质上是正确的。这是确切的详细信息。
让我们一次一步。我们有一个调用:
"test".Select<char, Tuple<char>>(Tuple.Create);
重载解析必须确定调用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>>)
现在,我们面临的第一个问题是应聘者是否适用?也就是说,是否存在从每个提供的参数到对应的形式参数类型的隐式转换?
一个好问题。显然,第一个参数是字符串“ receiver”,并且可以隐式转换为
IEnumerable<char>
。现在的问题是,第二个参数方法组“ Tuple.Create”是否可以隐式转换为形式参数类型Func<char,Tuple<char>>
和Func<char,int, Tuple<char>>
。方法组何时可以转换为给定的委托类型?如果给定的参数类型与委托的形式参数类型相同,则重载解析成功后,方法组可转换为委托类型。
也就是说,如果在给定类型为“ A”的表达式“ someA”的情况下成功调用了形式为
Func<A, R>
的重载解析,则M可转换为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转换的语言长很多;如果同时添加它们,我想他们的规则应该是一致的。
关于c# - 重载的方法组参数会混淆重载的解析?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37031076/