首页 文章

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

提问于
浏览
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 类型 .

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

  • 为method-group参数指定一个type-argument: Tuple.Create<char> (也许这实际上是一个类型推断问题?) .

  • 使参数成为lambda表达式而不是方法组: x => Tuple.Create(x) . (在 Select 电话上播放类型推理很好) .

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

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

这里的确切问题是什么?

2 回答

  • 20

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

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

    这里的确切问题是什么?

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

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

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

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

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

    特别是, candidates 中的两个是:

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

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

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

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

    也就是说,如果 M(someA) 形式的调用上的重载解析成功,则M可转换为 Func<A, R> ,给定'A'类型的表达式'someA' .

    在调用 Tuple.Create(someChar) 时,重载解析是否成功?是;重载决议会选择 Tuple.Create<char>(char) .

    在调用 Tuple.Create(someChar, someInt) 时,重载解析是否成功?是的,重载决议会选择 Tuple.Create<char,int>(char, int) .

    由于在这两种情况下重载决策都会成功,因此方法组可以转换为两种委托类型 . The fact that the return type of one of the methods would not have matched the return type of the delegate is irrelevant; overload resolution does not succeed or fail based on return type analysis .

    有人可能会合理地说,从方法组到委托类型的可转换性应该基于返回类型分析成功或失败,但是这是一个合理的选择 .

    因此我们有两个适用的候选人 . 有什么方法可以决定哪个比另一个更好?该规范指出转换为更具体的类型更好;如果你有

    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的主体是否在其中具有在表达式树中非法的内容 . 转换逻辑只是确保类型检出,而且确实如此 . 鉴于此,有另外一个,所以这是一个模棱两可的问题 .

    你注意到了

    "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转换确实考虑了返回表达式's type with the target delegate'的返回类型的兼容性 . 不幸的是,方法组和lambda表达式使用两种略有不同的算法来确定可转换性,但我们现在仍然坚持使用它 . 请记住,方法组转换的语言比lambda转换要长很多;如果他们同时加入,我想他们的规则会保持一致 .

  • 5

    我猜测编译器被Tuple.Create <char,int>(char,int)搞糊涂了,但它的返回类型是错误的:它返回一个二元组 .

    在重载决策期间考虑的返回类型是否为't part of the method signature, so it isn't;只有在选择过载后才会进行验证 . 因此,就编译器所知, Tuple.Create<char, int>(char, int) 是一个有效的候选者,它既不比 Tuple.Create<char>(char) 更好也不差,所以编译器无法决定 .

相关问题