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

问题描述 投票:26回答:2

请考虑以下代码段:

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)版本。这也导致拳击操作。

这种行为的原因是什么?

c# inheritance override
2个回答
29
投票

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

引自Eric Lippert

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

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


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

Update

这就是为什么我喜欢stackoverflow,它是一个非常好的学习场所。

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

7.6.5.1方法调用

...

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

请参阅Eric的帖子答案https://stackoverflow.com/a/52670391/1612975,了解有关此处的最新信息以及规格的相应部分

Original

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

Which can be shown here

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


其他资源

Eric Lippert, Closer is better

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

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

13
投票

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

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

如果D的作者对M的过载造成了动物,他们说我比B的作者更了解如何处理动物,包括长颈鹿。当我们给D.M(长颈鹿)打电话给D.M(动物)而不是B.M(长颈鹿)时,我们应该期待重载解决。

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

  • 打电话给D.M(长颈鹿)应该去B.M(长颈鹿),因为长颈鹿比动物更具体
  • 对D.M(Giraffe)的调用应该转到D.M(动物),因为D比B更具体

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

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.