C#方法覆盖分辨率怪异

时间:2018-10-05 08:59:15

标签: c# inheritance override

请考虑以下代码段:

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);
    }
}

令人惊讶的输出是:

Derived.Foo(object)

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

这种行为的原因是什么?

2 个答案:

答案 0 :(得分:29)

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

  

引用Eric Lippert

     

如果派生级别更高的类上的任何方法都是适用的候选方法,则它   它自动比派生较少的类上的任何方法都要好,甚至   如果来源较少的方法具有更好的签名匹配。

原因是因为该方法(签名匹配更好)可能已在更高版本中添加,从而引入了“ brittle base class”失败


注意 :这是C#规范中相当复杂/深入的部分,它遍地都是。但是,您遇到的问题的主要部分如下所述

更新

这就是为什么我喜欢stackoverflow,这是一个学习的好地方。

我引用了方法调用的运行时处理部分。问题是关于编译时重载分辨率的问题,应该存在的地方。

  

7.6.5.1方法调用

     

...

     

候选方法集减少为仅包含来自   最派生的类型:对于集合中的每个方法C.F,其中C是   声明方法F的类型,所有在基中声明的方法   类型的C从集合中删除。此外,如果C是类类型   除了对象,在接口类型中声明的所有方法都是   从集合中删除。 (仅当   方法组是对类型参数进行成员查找的结果   具有除对象和非空之外的有效基类   有效的界面集。)

请参阅Eric的帖子https://stackoverflow.com/a/52670391/1612975,以获取有关此处发生的情况以及规范的适当部分的完整详细信息

原始

C# 语言规格 版本5.0

  

7.5.5函数成员调用

     

...

     

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

     

...

     

如果M是用引用类型声明的实例函数成员:

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

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

  public interface ITest
  {
     void Foo(int x);
  }

Which can be shown here

关于接口,在考虑实现重载行为以防止出现脆性基类时确实有意义


其他资源

Eric Lippert, Closer is better

  

我今天要谈的C#中的重载解析方面是   实际上是判断一个潜在超负荷的基本规则   在给定的呼叫站点上要比另一个更好:总是更接近   比远处更好。有多种表征方法   C#中的“紧密度”。让我们从最接近的地方开始,然后走出自己的路:

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

答案 1 :(得分:13)

可接受的答案是正确的(除了它引用了规范的错误部分),但是它是从规范的角度进行解释,而不是给出合理性来说明为什么规范是好的

让我们假设有基类B和派生类D。B有一个采用长颈鹿的方法M。现在,请记住,假设,D的作者了解B的公共成员和受保护成员的一切。换句话说,D的作者必须比B的作者了解更多 ,因为 D是在B之后写的,,而 D则是将B扩展到a的。 B 尚未处理的情况。因此,我们应该相信,D的作者比B的作者在实现D的 all 功能方面做得更好。

如果D的作者超载了M并带走了Animal,他们说的是我比B的作者更了解如何处理动物,其中包括长颈鹿。当给D.M(Giraffe)而不是B.M(Giraffe)的调用时,我们应该期望过载解析。

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

  • 致电D.M(长颈鹿)应该转到B.M(长颈鹿),因为长颈鹿比动物更具体
  • 调用D.M(长颈鹿)应该转到D.M(动物),因为D比B更具体

两个理由都与特异性有关,那么哪个理由更好? 我们没有在Animal上调用任何方法!我们在D上调用该方法,因此那个特异性应该是获胜的方法。 接收器的特异性远比其任何参数的特异性重要得多。 可以使用参数类型来打破平局。重要的是要确保选择最具体的 receiver ,因为该方法是后来由对D打算处理的情况有更多了解的人编写的。

>

现在,您可能会说,如果D的作者也重写了B.M(Giraffe),该怎么办?在这种情况下,为什么调用D.M(长颈鹿)应该调用D.M(动物)有两个论点。

首先,D的作者应该知道可以用长颈鹿来调用DM(动物) ,并且必须将其写成正确的事情< / em>。因此,从用户的角度来看,是将呼叫解析为D.M(Animal)还是B.M(Giraffe)都无关紧要,因为正确地编写了D即可执行正确的操作。

第二,D的作者是否重写了B的方法,这是D的实现细节,而不是公共表面区域的部分。换句话说,如果更改是否重写方法更改选择哪个方法,这将是非常奇怪的。想象一下,如果您要在一个版本中的某个基类上调用方法,然后在下一个版本中,基类的作者对方法是否被覆盖进行了微小的更改;您不会期望派生类中的重载分辨率 会发生变化。 C#的设计经过精心设计,可以防止此类故障。