C#方法重载决策没有选择具体的通用覆盖

时间:2017-03-29 23:09:26

标签: c# generics overload-resolution

这个完整的C#程序说明了这个问题:

public abstract class Executor<T>
{
    public abstract void Execute(T item);
}

class StringExecutor : Executor<string>
{
    public void Execute(object item)
    {
        // why does this method call back into itself instead of binding
        // to the more specific "string" overload.
        this.Execute((string)item);
    }

    public override void Execute(string item) { }
}

class Program
{
    static void Main(string[] args)
    {
        object item = "value";
        new StringExecutor()
            // stack overflow
            .Execute(item); 
    }
}

我遇到了一个StackOverlowException,我追溯到这个调用模式,我试图将调用转发给更具体的重载。令我惊讶的是,调用并没有选择更具体的重载,而是回调自身。它显然与基类型是通用的有关,但我不明白为什么它不会选择执行(字符串)重载。

有没有人对此有任何见解?

上面的代码被简化以显示模式,实际结构有点复杂,但问题是一样的。

3 个答案:

答案 0 :(得分:30)

在C#规范5.0,7.5.3重载决议:

中看到了这一点
  

重载决策选择要在C#中的以下不同上下文中调用的函数成员:

     
      
  • 调用在invocation-expression(第7.6.5.1节)中命名的方法。
  •   
  • 调用在object-creation-expression(第7.6.10.1节)中命名的实例构造函数。
  •   
  • 通过元素访问(第7.6.6节)调用索引器访问器。
  •   
  • 调用表达式中引用的预定义或用户定义的运算符(第7.3.3节和第7.3.4节)。
  •   
     

这些上下文中的每一个都定义了候选函数成员集   和自己独特方式的参数列表,如中所述   上面列出的部分中的详细信息。例如,一组   方法调用的候选者不包括标记的方法   覆盖(第7.4节),并且基类中的方法不是候选者(如果有的话)   派生类中的方法适用(§7.6.5.1)。

当我们看7.4:

  

在类型T中使用K类型参数的名称N的成员查找按如下方式处理:

     

•首先,确定一组名为N的可访问成员:

     
      
  • 如果T是一个类型参数,那么该集合就是各组的联合   在T中指定为主要约束或次要约束(第10.1.5节)的每种类型中名为N的可访问成员,以及在对象中名为N的可访问成员集。

  •   
  • 否则,该集由T中名为N的所有可访问(§3.5)成员组成,包括继承成员和对象中可访问成员N。如果T是构造类型,则是成员集   是通过替换§10.3.2中描述的类型参数获得的。   包含覆盖修饰符的成员将从集合中排除。

  •   

如果删除override,编译器会在您投射项目时选择Execute(string)重载。

答案 1 :(得分:23)

正如Jon Skeet的article on overloading中所提到的,当在类中调用一个方法时,该方法也会覆盖基类中具有相同名称的方法,编译器将始终采用类内方法而不是覆盖,无论类型的“特异性”如何,只要签名是“兼容的”。

Jon接着指出,这是避免跨越继承边界的过载的一个很好的论据,因为这正是可能发生的意外行为。

答案 2 :(得分:16)

正如其他答案所指出的那样,这是设计上的。

让我们考虑一个不太复杂的例子:

class Animal
{
  public virtual void Eat(Apple a) { ... }
}
class Giraffe : Animal
{
  public void Eat(Food f) { ... }
  public override void Eat(Apple a) { ... }
}

问题是为什么giraffe.Eat(apple)解析为Giraffe.Eat(Food)而不是虚拟Animal.Eat(Apple)

这是两条规则的结果:

(1)在解决重载时,接收器的类型比任何参数的类型更重要。

我希望很清楚为什么会出现这种情况。编写派生类的人比编写基类的人具有更多的知识,因为编写派生类的人使用基类,而不是相反。

撰写Giraffe的人说&#34;我有办法Giraffe任何食物&#34;这需要对长颈鹿消化内部的特殊了解。该信息不存在于基类实现中,它只知道如何吃苹果。

因此,重载决策应始终优先选择派生类的适用方法,而不是选择基类的方法,而不管参数类型转换的更好性。

(2)选择覆盖或不覆盖虚拟方法不是类的公共表面区域的一部分。这是一个私人实施细节。因此,在进行重载决策时,不必做出任何决定,这将根据是否覆盖方法而改变。

重载决议绝不能说&#34;我将选择虚拟Animal.Eat(Apple) ,因为它已被覆盖&#34;。

现在,你可能会说&#34;好吧,假设我在打电话时里面长颈鹿。&#34;代码里面 Giraffe拥有私人实施细节的所有知识,对吧?因此,当面对Animal.Eat(Apple)时,它可以决定调用虚拟Giraffe.Eat(Food)而不是giraffe.Eat(apple),对吧?因为它知道有一种实现能够理解吃苹果的长颈鹿的需求。

这比疾病更糟糕。现在我们的情况是相同的代码具有不同的行为,具体取决于它的运行位置!你可以想象在类之外调用giraffe.Eat(apple),重构它以使它在类中,并且突然可观察到的行为发生了变化!

或者,你可能会说,嘿,我意识到我的Giraffe逻辑实际上足够普遍移动到基类,但不是Animal,所以我将重构我的Giraffe代码:

class Mammal : Animal 
{
  public void Eat(Food f) { ... } 
  public override void Eat(Apple a) { ... }
}
class Giraffe : Mammal
{
  ...
}

现在所有对 giraffe.Eat(apple)内的Giraffe 的调用在重构后突然有不同的重载解析行为?那将是非常意外的!

C#是一种成功的语言;我们非常希望确保简单的重构,例如更改方法中覆盖方法的位置,不会导致行为的细微变化。

总结:

  • 重载决策优先于接收器优先于其他参数,因为调用知道接收器内部的专用代码比调用更通用的代码更好。
  • 在重载解析期间不考虑是否以及在何处覆盖方法;为了重载解析的目的,所有方法都被视为从不重写。它是一个实现细节,而不是该类型表面的一部分。
  • 解决了过载解决问题 - 当然模数可访问性! - 无论问题出现在代码中的哪个位置都是一样的。我们没有一种解决方案,其中接收器是包含代码的类型,另一种是当调用在不同类中时。

有关相关问题的其他想法,请访问:https://ericlippert.com/2013/12/23/closer-is-better/https://blogs.msdn.microsoft.com/ericlippert/2007/09/04/future-breaking-changes-part-three/