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

Mar*_*lug 45 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); 
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Sel*_*enç 31

看起来在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节).

这些上下文中的每一个都以其自己独特的方式定义候选函数成员集和参数列表,如上面列出的部分中详细描述的.例如,方法调用的候选集不包括标记为override的方法(第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)在转换项目时选择重载.

  • @Mark Jon Skeet的[文章](http://csharpindepth.com/Articles/General/Overloading.aspx)描述了原因,"这样做的原因是为了降低脆弱基类问题的风险,其中引入了基类的新方法可能会导致从它派生的类的消费者出现问题." (14认同)
  • 当然,我的后续问题是:为什么?我确信有一个很好的理由,但这对我来说似乎是令人惊讶的行为. (3认同)

Dav*_*d L 24

正如Jon Skeet 关于重载文章中所提到的,当在类中调用一个方法同时覆盖基类中具有相同名称的方法时,编译器将始终采用类内方法而不是覆盖,而不管"具体性"如何"类型,只要签名是"兼容的".

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

  • 实际上,它与通用基类没有任何关系.我可以在没有这方面的情况下重新编写代码.这对我来说非常令人惊讶. (2认同)
  • 正确,泛型参数在这种情况下是一个红色鲱鱼. (2认同)

Eri*_*ert 17

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

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

class Animal
{
  public virtual void Eat(Apple a) { ... }
}
class Giraffe : Animal
{
  public void Eat(Food f) { ... }
  public override void Eat(Apple a) { ... }
}
Run Code Online (Sandbox Code Playgroud)

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

这是两条规则的结果:

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

我希望很明显为什么必须如此.编写派生类的人比编写基类的人具有更多的知识,因为编写派生类的人使用基类,而不是相反.

写作的人Giraffe说"我有办法Giraffe任何食物 ",需要特别了解长颈鹿消化的内部.基类实现中不存在该信息,只知道如何吃苹果.

因此,无论参数类型转换的更好性如何,重载解析应始终优先选择派生类的适用方法而不是选择基类的方法.

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

重载决议绝不能说"我要选择虚拟,Animal.Eat(Apple) 因为它被覆盖 ".

现在,您可能会说"好吧,假设我在打电话时我长颈鹿里面." 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
{
  ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

加起来:

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

有关相关问题的其他想法可以在这里找到:https://ericlippert.com/2013/12/23/closer-is-better/https://blogs.msdn.microsoft.com/ericlippert/2007/09/ 04 /未来的磨合修改部分三/