在派生类中重载基类方法

13

我在使用C#进行编程时,想验证一下这篇文章中描述的C++行为是否也适用于C#:http://herbsutter.com/2013/05/22/gotw-5-solution-overriding-virtual-functions/ 然后我遇到了一个非常奇怪的问题:

public class BaseClass
{
    public virtual void Foo(int i)
    {
        Console.WriteLine("Called Foo(int): " + i);
    }

    public void Foo(string i)
    {
        Console.WriteLine("Called Foo(string): " + i);
    }
}

public class DerivedClass : BaseClass
{
    public void Foo(double i)
    {
        Console.WriteLine("Called Foo(double): " + i);
    }
}

public class OverriddenDerivedClass : BaseClass
{
    public override void Foo(int i)
    {
        base.Foo(i);
    }

    public void Foo(double i)
    {
        Console.WriteLine("Called Foo(double): " + i);
    }
}

class Program
{
    static void Main(string[] args)
    {
        DerivedClass derived = new DerivedClass();
        OverriddenDerivedClass overridedDerived = new OverriddenDerivedClass();

        int i = 1;
        double d = 2.0;
        string s = "hi";

        derived.Foo(i);
        derived.Foo(d);
        derived.Foo(s);

        overridedDerived.Foo(i);
        overridedDerived.Foo(d);
        overridedDerived.Foo(s);
    }
}

输出

Called Foo(double): 1
Called Foo(double): 2
Called Foo(string): hi
Called Foo(double): 1
Called Foo(double): 2
Called Foo(string): hi

显然,它偏好于将隐式转换的int转为double,而不是来自基类的更具体的Foo(int)。或者它隐藏了来自基类的Foo(int)?但这样的话:为什么Foo(string)没有被隐藏呢?感觉非常不一致......无论我是否覆盖Foo(int),结果都是相同的。有人能解释一下这里发生了什么吗?

(是的,我知道在派生类中重载基本方法是不好的实践——Liskov等——但我仍然不希望在OverriddenDerivedClass中调用Foo(int)!)


4
好的,我会尽力进行翻译,请提供原文。 - ose
3
我知道在派生类中重载基类方法是不良实践,遵循Liskov原则。但什么情况下可以重写基类方法呢?无论如何+1,因为我也想知道答案。 - bas
1
@DimitarDimitrov 祈求 Skeet 的庇佑,让他在智慧的圣光中来祝福我们。 - Nolonar
2
说到伟大的Skeet:https://dev59.com/questions/ZXE85IYBdhLWcg3wgDvd - Corak
2
没有比Eric Lippert回答Jon Skeet的脑筋急转弯更好的选择了。 - Corak
显示剩余8条评论
1个回答

10
为了解释OverriddenDerivedClass示例的工作原理:
请参阅此处的C#规范以进行成员查找:http://msdn.microsoft.com/en-us/library/aa691331%28VS.71%29.aspx 它定义了如何执行查找。
特别是,请查看以下部分:
首先,构造T中声明的所有可访问(第3.5节)名称为N的成员及其基类型(第7.3.1节)的集合。 排除包括override修饰符的声明在内的集合。
在您的情况下,NFoo()。由于排除包括override修饰符的声明在内的集合,因此排除了override Foo(int i)
因此,仅保留未被覆盖的Foo(double i),因此调用它。
这就是OverriddenDerivedClass示例的工作方式,但这不是DerivedClass示例的说明。
要解释这一点,请查看规范的此部分:
接下来,从集合中删除被其他成员隐藏的成员。 DerivedClass中的Foo(double i)正在隐藏来自基类的Foo(int i),因此将其从集合中删除。
这里棘手的部分是:
所有在S的基类型中声明的与M具有相同签名的方法都将从集合中删除。
您可能会说“但等等! Foo(double i)的签名与Foo(int i)不同,因此不应将其从集合中删除!”。
但是,由于int到double存在隐式转换,因此它被认为具有相同的签名,因此从集合中删除了Foo(int i)

((BaseClass)derived).Foo(i) - Corak
好的,谢谢你。但是确实:"...和T的基本类型(第7.3.1节)...意味着BaseClass :: Foo(int)将在范围内。我能想象的是,集合看起来像:DerivedClass :: Foo(double),BaseClass :: Foo(int)。然后当它接受整数参数时,它知道可以将整数隐式转换为double,并且不会评估BaseClass :: Foo(int)。然而,只有当具有Foo(double)Foo(int)的普通类具有这样的集合时才成立:NormalClass:Foo(int),NormalClass:Foo(double)。我们能验证一下吗? - Nebula
@Nebula 抱歉,我不太确定你的意思... 但是,基类 Foo(int) 在作用域内,但由于它与派生类 Foo(double) 具有相同的签名(因为从 int 到 double 存在隐式转换),所以派生类的 Foo(double) 导致基类的 Foo(int) 被从集合中移除。 - Matthew Watson
@MatthewWatson 是的,事实上看起来是这样发生的。然而,对我来说不清楚的是如何做出这个决定:派生方法是否比基类方法优先?即使类型必须被转换(即“强制”)以适应签名?换句话说,编译器是通过比较派生和基类的签名来做出决策,还是首先查看派生列表以查看是否可以进行匹配(使用隐式转换等任何匹配),如果可以,则完全忽略基类? - Nebula
@Nebula 它完全遵循规范中概述的步骤(请参见我发布的完整规范链接)。该规范未定义的唯一重要事项是,来自具有与派生方法中相应参数类型具有隐式转换的参数类型的基类的方法被认为具有相同的签名。我在规范中找不到这个...但是Eric Lippert的博客解释得更好:http://blogs.msdn.com/b/ericlippert/archive/2007/09/04/future-breaking-changes-part-three.aspx - Matthew Watson
@MatthewWatson 谢谢您先生!确实是非常好的答案。再次在C#的美妙世界中获得了更多的知识 :-) - Nebula

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