C#方法覆盖了解决方案的怪异

Imp*_*rks 25 c# inheritance overriding

请考虑以下代码段:

using System;

class Base
{
    public virtual void Foo(int x)
    {
        Console.WriteLine("Base.Foo(int)");
    }
}

class Derived : Base
{
    public override void Foo(int x)
    {
        Console.WriteLine("Derived.Foo(int)");
    }

    public void Foo(object o)
    {
        Console.WriteLine("Derived.Foo(object)");
    }
}

public class Program
{
    public static void Main()
    {
        Derived d = new Derived();
        int i = 10;
        d.Foo(i);
    }
}
Run Code Online (Sandbox Code Playgroud)

令人惊讶的输出是:

Derived.Foo(object)
Run Code Online (Sandbox Code Playgroud)

我希望它能够选择被覆盖的Foo(int x)方法,因为它更具体.但是,C#编译器会选择非继承Foo(object o)版本.这也导致拳击操作.

这种行为的原因是什么?

AAA*_*ddd 28

这是规则,你可能不喜欢它......

引自Eric Lippert

如果更多派生类上的任何方法是适用的候选者,则它自动优于较少派生类上的任何方法,即使较少派生的方法具有更好的签名匹配.

原因是因为该方法(更好的签名匹配)可能已在更高版本中添加,从而引入了" 脆弱的基类 "故障


注意:这是C#规范中相当复杂/深入的部分,它会遍布整个地方.但是,您遇到的问题的主要部分如下所示

更新

这就是为什么我喜欢stackoverflow,这是一个学习教学的好地方,并且拥有业内一些最聪明的头脑,准备分享他们的知识和纠正误解,我总是愿意和荣幸地被他们纠正.这是一个完美的例子.

我引用了方法调用的运行时处理部分.问题是关于编译时的重载决议,应该是.

7.6.5.1方法调用

...

候选方法集合被简化为仅包含来自大多数派生类型的方法:对于集合中的每个方法CF,其中C是声明方法F的类型,所有在基本类型C中声明的方法都从集合.此外,如果C是除object之外的类类型,则从集合中删除在接口类型中声明的所有方法.(后一条规则仅在方法组是对具有除object之外的有效基类和非空有效接口集的类型参数进行成员查找的结果时才会生效.)

请参阅Eric的帖子回答/sf/answers/3686927401/,了解有关此处的最新信息以及规范的相应部分

原版的

C#语言规范版本5.0

7.5.5函数成员调用

...

函数成员调用的运行时处理包括以下步骤,其中M是函数成员,如果M是实例成员,则E是实例表达式:

...

如果M是在reference-type中声明的实例函数成员:

  • E被评估.如果此评估导致异常,则不执行进一步的步骤.
  • 参数列表按照§7.5.1中的描述进行评估.
  • 如果E的类型是值类型,则执行装箱转换(第4.3.1节)以将E转换为类型对象,并且在以下步骤中将E视为类型为对象.在这种情况下,M只能是System.Object的成员.
  • 检查E的值是否有效.如果E的值为null,则抛出System.NullReferenceException,并且不执行进一步的步骤.
  • 确定要调用的函数成员实现:
    • 如果E的绑定时类型是接口,则要调用的函数成员是由E引用的实例的运行时类型提供的M的实现.通过应用接口映射规则(第13.4.4节)确定此函数成员,以确定由E引用的实例的运行时类型提供的M的实现.
    • 否则,如果M是虚函数成员,则要调用的函数成员是由E引用的实例的运行时类型提供的M的实现.通过应用用于确定最多派生实现的规则来确定该函数成员( M的§10.6.3)关于E引用的实例的运行时类型.
    • 否则,M是非虚函数成员,要调用的函数成员是M本身.

阅读规范之后有趣的是,如果使用描述方法的接口,编译器将选择过载签名,然后按预期工作

  public interface ITest
  {
     void Foo(int x);
  }
Run Code Online (Sandbox Code Playgroud)

这可以在这里显示

关于接口,当考虑实现重载行为以防止脆弱的基类时,它确实有意义


其他资源

Eric Lippert,Closer更好

今天我想谈谈的C#中的重载解析方面实际上是一个基本规则,通过该规则,一个潜在的重载被判断为比给定呼叫站点的另一个更好:更接近总是比远离更好.有很多方法可以表征C#中的"亲密度".让我们从最近的地方开始,然后走出去:

  • 首先在派生类中声明的方法比在基类中首先声明的方法更接近.
  • 嵌套类中的方法比包含类中的方法更接近.
  • 接收类型的任何方法都比任何扩展方法更接近.
  • 在嵌套命名空间中的类中找到的扩展方法比在外部命名空间中的类中找到的扩展方法更接近.
  • 在当前命名空间的类中找到的扩展方法比在using指令提到的命名空间中的类中找到的扩展方法更接近.
  • 在using指令中提到的命名空间中的类中找到的扩展方法(其中指令位于嵌套命名空间中)比在using指令中提到的命名空间中的类中找到的扩展方法更接近,其中该指令位于外部命名空间中.

  • @KonradRudolph:现在,你的观点"我不明白规范如何适用"很好,这是因为萨鲁曼引用了规范的错误部分.这是方法调用的**运行时处理**的部分.问题是**编译时超载分辨率**. (2认同)

Eri*_*ert 13

接受的答案是正确的(除了它引用了规范的错误部分这一事实),但它从规范的角度解释了事情,而不是说明规范为什么是好的理由.

假设我们有基类B和派生类D. B有一个采用长颈鹿的方法M. 现在,记住,通过假设,D的作者知道B的公共和受保护成员的一切.换句话说:D的作者必须比B的作者知道更多,因为D是在B之后写的,并且D被编写为将B扩展到尚未由B处理的场景.因此,我们应该相信D的作者在实现D的所有功能方面比B的作者做得更好.

如果D的作者对M的过载造成了动物,他们说我比B的作者更了解如何处理动物,包括长颈鹿.当我们调用DM(Giraffe)来调用DM(Animal)而不是BM(Giraffe)时,我们应该期望重载解析.

让我们换一种说法:我们有两种可能的理由:

  • 呼叫DM(长颈鹿)应该去BM(长颈鹿),因为长颈鹿比动物更具特异性
  • 对DM(Giraffe)的调用应该转到DM(动物),因为D比B更具体

两种理由都是关于特异性的,那么哪种理由更好?我们没有在Animal上调用任何方法!我们呼吁d的方法,使特异性应该是赢得了一个.接收器的特异性远远超过其任何参数的特异性. 参数类型用于打破平局.重要的是确保我们选择最具体的接收器,因为该方法后来由更多了解D打算处理的场景的人编写.

现在,你可能会说,如果D的作者也覆盖了BM(长颈鹿)怎么办?在这种情况下,为什么呼叫DM(长颈鹿)应该调用DM(动物)有两个论点.

首先,D的作者应该知道DM(动物)可以用长颈鹿调用,并且必须写成正确的东西.因此,从用户的角度来看,无论呼叫是解析为DM(动物)还是BM(长颈鹿),都应该无关紧要,因为D已被正确编写以做正确的事情.

其次,D的作者是否覆盖了B的方法是D 的实现细节,而不是公共表面区域的一部分.换句话说:如果改变方法是否被覆盖会改变选择哪种方法,那将是非常奇怪的.想象一下,如果你在一个版本的某个基类上调用一个方法,那么在下一个版本中,基类的作者对方法是否被覆盖进行了微小的改变; 您不希望派生类中的重载解析更改.C#经过精心设计,可以防止出现这种故障.