在继承类中重载方法

14

我开始明白我不明白正在发生什么。在C#中有以下行为:

public class Base
{
    public void Method(D a)
    {
        Console.WriteLine("public void Method(D a)");
    }
}

public class Derived: Base
{
    public void Method(B a)
    {
        Console.WriteLine("public void Method(B a)");
    }
}

public class B { }

public class D: B { }

class Program
{
    static void Main(string[] args)
    {
        Derived derived = new Derived();
        D d = new D();

        derived.Method(d);
    }
}

它将打印

public void Method(B a)

而不是

public void Method(D a)

这很令人惊讶。我认为这种行为的原因是方法表的实现。CLR如果在当前类型中找到相应的方法,就不会在基类中搜索方法。我认为他们试图提高性能。

但是下面的代码完全让我失望:

public class Base
{
    public virtual void Method(D a)
    {
        Console.WriteLine("public void Method(D a)");
    }
}

public class Derived: Base
{
    public override void Method(D a)
    {
        Console.WriteLine("public override void Method(D a)");
    }

    public void Method(B a)
    {
        Console.WriteLine("public void Method(B a)");
    }

}

public class B { }

public class D: B { }

class Program
{
    static void Main(string[] args)
    {
        Derived derived = new Derived();
        D d = new D();

        derived.Method(d);
    }
}

它会打印

public void Method(B a)

而不是

public override void Method(D a)

这太可怕且难以预测了。

有人可以解释一下吗?

我认为方法表中仅包含当前类型实现的方法(不包括重写方法),并且CLR在找到任何可调用的方法后停止查找相应的方法。我对吗?


3
在方法表中,它会寻找在方法被调用的特定类型中具有相应签名的方法,并且由于继承关系,它发现匹配项为d也是b,因此它忽略了被覆盖的方法。 - Ehsan Sajjad
@EhsanSajjad 这个问题是为什么编译器选择了一个重载而不是另一个。两者都是有效的方法调用,但编译器选择了其中一个。 - Servy
1
CLR 会在找到任何可调用的方法后立即停止寻找相应的方法,我说得对吗?但这与 CLR 无关。这完全是 C# 编译器基于 C# 规范所做出的编译时决策,而不是运行时做出的决策。 - Servy
2个回答

15

我开始意识到我不明白正在发生什么。

因此,智慧始于这里。

这很糟糕且非常不可预测。

它既不糟糕也不可预测。相反,该特性旨在通过消除脆弱的基类故障的原因来减少不可预测性。

有人能解释一下吗?

请参阅我2007年关于此主题的文章。

https://blogs.msdn.microsoft.com/ericlippert/2007/09/04/future-breaking-changes-part-three/

简而言之是:派生类中的方法总是比基类中的方法更好;编写派生类的人更了解对象语义并处理的是更特定的情况,而编写基类的人则不然。

Servy指出我没有涉及您的第二点。为什么派生类中的覆盖方法不使“D”方法成为“派生类中的”方法?

虚方法被认为是在声明它们的类中的方法,而不是在最近覆盖它们的类中的方法。为什么?因为选择是否覆盖是类的实现细节,而不是公共表面的一部分。

我的意思是,想想看。你写了一些代码,它可以正常工作,然后你说,嘿,我要在类型层次结构中的某个地方创建一个新的(或删除一个旧的!)该方法的覆盖,结果突然影响到其他地方的重载解析?这正是C#试图消除的脆弱性。

请记住,C# 在设计时非常注重团队协作,因为它是为了在多人编辑代码的情况下而设计的。许多现代编程语言奇怪地设计成好像只有一个人编写所有代码,并且第一次就完美无缺;这并不现实。C# 的一些较为独特的功能,比如你所发现的那个,是为了帮助你在其他人编辑你的基类时保持程序行为的可预测性。


1
@Servy:虚方法被认为是声明它的类型的方法,而不是重载它的类型。但这是一个微妙的问题。我会在答案中添加注释。 - Eric Lippert

1

我可以看到这里有些混淆。Eric的回答如果能用一个例子更好地说明就好了。如果你按照以下方式更改代码...

public class Base
{
    public virtual void Method(D a)
    {
        Console.WriteLine("public void Method(D a)");
    }

    public void Method(B a)
    {
        Console.WriteLine("public void Method(B a)");
    }
}

public class Derived : Base
{
    public override void Method(D a)
    {
        Console.WriteLine("public override void Method(D a)");
    }

}

public class B { }

public class D : B { }

输出结果变成了"public override void Method(D a)"。
为什么呢?
正如Eric的文章所述...

如果派生类中有任何一个方法适用,则基类中的方法不是候选方法

由于Method(D)和Method(B)现在都在基类中,Method(D)已成为最接近的匹配项,在派生类中被重写,因此输出结果为此。
对于新手来说可能很困惑,对于经验丰富的开发人员也很困惑。我认为这里要点是设计不良,我不是在谈论CLR或C#编译器,它已经尽力帮助你了,我是在谈论您示例中的类。
我认为许多新手在面向对象编程中犯的常见错误是过度使用继承。 继承通常不是OOD中使用的首选关系。 当您开始研究OOP设计模式时,您会注意到许多模式不使用继承。
为什么?因为你的例子说明了使用继承可能会导致混淆,这被称为脆弱的继承。设计模式更常使用聚合和组合将问题空间分解成更易处理、可扩展和可管理的解决方案。
继承是一个强大的构造,像大多数强大的东西一样,应该谨慎地使用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接