C#方法重载决议不select具体的generics覆盖

这个完整的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); } } 

我遇到了一个StackOverlowExceptionexception,我追溯到这个调用模式,我试图将调用转发给更具体的重载。 令我惊讶的是,调用并不是select更具体的超载,而是回到自身。 这显然与基types是generics有关,但我不明白为什么它不会select执行(string)重载。

有没有人有任何见解?

上面的代码被简化来显示模式,实际的结构稍微复杂一些,但问题是一样的。

看起来像是在C#规范5.0,7.5.3中提到的重载parsing:

重载parsingselect在C#中以下不同的上下文中调用的函数成员:

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

这些上下文中的每一个以其自己独特的方式定义候选函数成员集合和参数列表,如在上面列出的部分中详细描述的。 例如,方法调用的候选集合不包括标记为override的方法(第7.4节),并且如果派生类中的任何方法适用 (第7.6.5.1 节),则基类中的方法不是候选方法

当我们看7.4时:

typesT中具有Ktypes参数的名称N的成员查找处理如下:

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

  • 如果T是一个types参数,那么这个集合就是集合的集合
    在每个被指定为T的主要约束或次要约束(§10.1.5)的types中,名称为N的可访问成员以及在对象中名为N的可访问成员集。

  • 否则,集合由T中所有可访问的(§3.5)名为N的成员组成,包括inheritance的成员和对象中可访问的成员名称N. 如果T是一个构造types,那么通过将types参数replace成第10.3.2节中描述的那样获得成员集合。 包含超驰修饰符的成员从该组中排除。

如果删除override则编译器override在您Execute(string)该项目时selectExecute(string)过载。

正如Jon Skeet 关于重载的文章中提到的那样,当在一个类中调用一个方法,这个方法也会覆盖一个基类中同名的方法时,编译器将总是采用in-class方法而不是覆盖,不pipe“特定性“的types,只要签名是”兼容的“。

Jon继续指出,这是避免在inheritance边界上重载的一个很好的论点,因为这正是可能发生的意外行为。

正如其他答案已经指出,这是devise。

我们来考虑一个较简单的例子:

 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)parsing过载时,接收方的types比任何参数的types都重要。

我希望这是明确的,为什么这是必然的。 编写派生类的人比编写基类的人有更多的知识,因为编写派生类的人使用基类,而不是相反。

Giraffe的人说:“我有办法让Giraffe任何食物 ”,而且需要长颈鹿消化内部的专门知识。 这个信息并不存在于基类实现中,它只知道如何吃苹果。

因此, 无论参数types转换的更好性如何,重载parsing应始终优先select派生类的适用方法而不是select基类的方法

(2)select覆盖或不覆盖虚拟方法不是类的公共表面区域的一部分。 这是一个私人的实现细节。 因此,在做重载决议时,不能做出决定,重决决定取决于是否重写方法。

重载parsing决不能说“我将select虚拟Animal.Eat(Apple) 因为它被覆盖 ”。

现在,你可能会说:“好吧,当我打电话的时候,我长颈鹿里面 。” 长颈鹿内部的代码拥有私有实现细节的所有知识,对吗? 所以当面对giraffe.Eat(apple)时,可以决定调用虚拟Animal.Eat(Apple)而不是Giraffe.Eat(Food)giraffe.Eat(apple) ,对吗? 因为它知道有一个实现了解吃苹果的长颈鹿的需求。

这是一种比疾病更糟糕的治疗方法。 现在我们有一种情况,即相同的代码根据运行的地方而有不同的行为! 你可以想象在类之外有一个对giraffe.Eat(apple)的调用, giraffe.Eat(apple)重构以便它在类内部,并且突然观察到的行为改变了!

或者,你可能会说,嘿,我意识到我的长颈鹿逻辑实际上已经足够普遍的移动到一个基类,但不是动物,所以我要重构我的Giraffe代码来:

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

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

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

加起来:

  • 重载parsing优先考虑其他参数的接收者,因为调用知道接收者内部特定的代码比调用更多的通用代码更好。
  • 重载决议中不考虑方法是否被忽略; 所有的方法都被视为重载parsing的目的。 这是一个实现细节,不是types的一部分。
  • 重载解决问题得到解决 – 当然模访问! – 不pipe代码出现问题的地方,都是一样的。 我们没有一个parsingalgorithm,其中接收者是包含代码的types,另一个是在不同类别的调用时。

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