这个完整的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,我追溯到这个调用模式,我试图将调用转发给更具体的重载。令我惊讶的是,调用并没有选择更具体的重载,而是回调自身。它显然与基类型是通用的有关,但我不明白为什么它不会选择执行(字符串)重载。
有没有人对此有任何见解?
上面的代码被简化以显示模式,实际结构有点复杂,但问题是一样的。
答案 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/