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)在转换项目时选择重载.
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 /未来的磨合修改部分三/
| 归档时间: |
|
| 查看次数: |
2309 次 |
| 最近记录: |