【问题标题】:Overloaded method-group argument confuses overload resolution?重载的方法组参数混淆了重载解析?
【发布时间】:2011-03-05 12:25:06
【问题描述】:

对重载的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&lt;char&gt;(char)。不应应用第二个重载,因为没有一个 Tuple.Create 重载可以转换为预期的 Func&lt;char,int,Tuple&lt;char&gt;&gt; 类型。我猜测编译器被Tuple.Create&lt;char, int&gt;(char, int) 弄糊涂了,但它的返回类型是错误的:它返回一个二元组,因此不能转换为相关的Func 类型。

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

  1. 为方法组参数指定类型参数:Tuple.Create&lt;char&gt;(也许这实际上是类型推断问题?)。
  2. 使参数成为 lambda 表达式而不是方法组:x =&gt; Tuple.Create(x)。 (在 Select 调用中可以很好地进行类型推断)。

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

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

这里的确切问题是什么?

【问题讨论】:

    标签: c# c#-4.0 overload-resolution method-group


    【解决方案1】:

    首先,我注意到这是以下内容的副本:

    Why is Func<T> ambiguous with Func<IEnumerable<T>>?

    这里的确切问题是什么?

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

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

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

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

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

    特别是,两个候选人是:

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

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

    一个很好的问题。很明显,第一个参数是“接收者”,一个字符串,它可以隐式转换为IEnumerable&lt;char&gt;。现在的问题是,第二个参数(方法组“Tuple.Create”)是否可以隐式转换为形参类型Func&lt;char,Tuple&lt;char&gt;&gt;Func&lt;char,int, Tuple&lt;char&gt;&gt;

    方法组何时可以转换为给定的委托类型? 当重载解析成功给定与委托的形参类型相同类型的参数时,方法组可转换为委托类型

    也就是说,如果在给定类型为“A”的表达式“someA”的情况下,对M(someA) 形式的调用的重载解析成功,则 M 可转换为 Func&lt;A, R&gt;

    调用Tuple.Create(someChar) 是否会成功解决重载问题?是的;重载决议会选择Tuple.Create&lt;char&gt;(char)

    调用Tuple.Create(someChar, someInt) 是否会成功解决重载问题?是的,重载决议会选择Tuple.Create&lt;char,int&gt;(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 转换要长得多。如果它们同时添加,我想它们的规则会保持一致。

    【讨论】:

    • Eric,关于可转换性“我们坚持使用[两种略有不同的算法]” 的声明表明这里有向后兼容性考虑,是吗?假设允许方法组转换使用返回类型分析进行解析会造成现有代码会导致不同的重载选择的情况。但是,我无法构建一个实际会发生这种情况的场景——真的有这种情况吗?或者这更多是为了平衡低价值用例的风险/回报?
    • 我非常感谢这个答案的细节;我想我现在对这一点有了更好的理解。实际上,我对我的代码有一半的期望,因为我的印象是方法组的返回类型在 C# 4 中得到了更多的“关注”,但大概这只是为了类型推断?无论如何,对我来说,重载解析和类型推断有一点黑魔法。我的直觉经常让我失望。
    • 另一方面,我觉得有点令人费解的是,C# 倾向于始终如此强烈地将以下两个方面分开:a) 用户可能意味着什么 b) 用户可能意味着合法。想让b)a)中发挥更大的作用是不是错了?
    • @Ani:方法组的返回类型现在在某些情况下用于方法类型推断。例如,如果你有方法 M(T t, Func) 和 int N(string),你说 M("hello", N) 那么首先我们推断 T 是字符串,然后我们对 N(someString) 进行重载解析,它成功了,然后我们推断 U 是 int。
    【解决方案2】:

    我猜编译器被Tuple.Create&lt;char, int&gt;(char, int)弄糊涂了,但是它的返回类型是错误的:它返回一个二元组。

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

    【讨论】:

    • 谢谢,这听起来很合理。您有任何参考资料可以证实这一点吗?
    • 这不仅是合理的,而且是准确的。 SO 的参考文献很困难,投票是匿名的。
    • @Hans:抱歉,没看懂你的评论。我说的是文档,如果不清楚的话。你的意思是什么?
    • @Ani,查看 C# 4 规范,§7.5.3.1(适用的函数成员)。没有任何规则提及返回类型,编译器只检查提供的参数是否与声明的参数匹配。在您的情况下,两种重载都适用。下一节(第 7.5.3.2 节)解释了用于确定更好的函数成员的规则,并且再次不考虑返回类型
    • @Thomas:这很模糊。你能解释得更清楚吗?请注意,Tuple.Create 的任何重载都不能转换为 Func&lt;char,int,Tuple&lt;char&gt;&gt;,因此仅从“编译器仅检查提供的参数是否与声明的参数匹配”这句话来看,Select 的第二个重载应该不再是调用的适用函数成员。显然,我刚才说的是错误的,但您的回答似乎没有解决它(以我理解的方式)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-18
    相关资源
    最近更新 更多