为啥这行得通?方法重载+方法覆盖+多态

Posted

技术标签:

【中文标题】为啥这行得通?方法重载+方法覆盖+多态【英文标题】:Why does this work? Method overloading + method overriding + polymorphism为什么这行得通?方法重载+方法覆盖+多态 【发布时间】:2010-12-22 10:30:09 【问题描述】:

在以下代码中:

public abstract class MyClass

public abstract bool MyMethod(
        Database database,
        AssetDetails asset,
        ref string errorMessage);


public sealed class MySubClass : MyClass

    public override bool MyMethod(
        Database database,
        AssetDetails asset,
        ref string errorMessage)
    
        return MyMethod(database, asset, ref errorMessage);
    

    public bool MyMethod(
        Database database,
        AssetBase asset,
        ref string errorMessage)
    
    // work is done here


其中 AssetDetails 是 AssetBase 的子类。

为什么第一个 MyMethod 在运行时在传递 AssetDetails 时调用第二个,而不是陷入无限的递归循环?

【问题讨论】:

你没有在你的MySubClass定义上忘记一个`:MyClass`吗? 1 - 你真的想把 MySubClass 作为一个内部类吗?为什么? 2 - 您能否举一个实际调用该方法以给出您所描述的行为的示例 MySubClass 不是内部类,不是。一个例子实际上是 mySubClassInstance.MyMethod(database, new AssetDetails(), ref msg); - 这命中第一个方法,然后传递给第二个 “mysubClassInstance”是如何声明的?是基础类型还是后代类型? (无论你放入什么类型) mysubClassInstance 被声明为后代类型。 【参考方案1】:

C# 将解析您对其他实现的调用,因为调用对象上的方法,该对象的类有自己的实现,而不是被覆盖或继承的方法。

这可能会导致微妙且难以发现的问题,就像您在此处展示的那样。

例如,试试这段代码(先阅读它,然后编译并执行它),看看它是否符合您的预期。

using System;

namespace ConsoleApplication9

    public class Base
    
        public virtual void Test(String s)
        
            Console.Out.WriteLine("Base.Test(String=" + s + ")");
        
    

    public class Descendant : Base
    
        public override void Test(String s)
        
            Console.Out.WriteLine("Descendant.Test(String=" + s + ")");
        

        public void Test(Object s)
        
            Console.Out.WriteLine("Descendant.Test(Object=" + s + ")");
        
    

    class Program
    
        static void Main(string[] args)
        
            Descendant d = new Descendant();
            d.Test("Test");
            Console.In.ReadLine();
        
    

请注意,如果您将变量的类型声明为 Base 而不是 Descendant,则调用将转到另一个方法,请尝试更改此行:

Descendant d = new Descendant();

到这里,然后重新运行:

Base d = new Descendant();

那么,那么,您实际上是如何设法拨打Descendant.Test(String) 的呢?

我的第一次尝试是这样的:

public void Test(Object s)

    Console.Out.WriteLine("Descendant.Test(Object=" + s + ")");
    Test((String)s);

这对我没有好处,而是一次又一次地调用 Test(Object) 以导致最终的堆栈溢出。

但是,以下工作。因为,当我们将 d 变量声明为 Base 类型时,我们最终调用了正确的虚拟方法,我们也可以使用这个技巧:

public void Test(Object s)

    Console.Out.WriteLine("Descendant.Test(Object=" + s + ")");
    Base b = this;
    b.Test((String)s);

这将打印出来:

Descendant.Test(Object=Test)
Descendant.Test(String=Test)

您也可以从外部执行此操作:

Descendant d = new Descendant();
d.Test("Test");
Base b = d;
b.Test("Test");
Console.In.ReadLine();

将打印出相同的内容。

但是首先需要意识到问题,这完全是另一回事。

【讨论】:

因为如果没有一个虚拟方法和一个非虚拟方法(同一个类中不能有两个相同的方法),或者在基类中没有一个方法,就没有办法进入这个烂摊子类在没有虚拟方法的情况下调用后代类中的方法,我认为这不会影响它。我仍然不确定为什么正如你所说的那样重要,有很多这样的微妙问题,并非所有问题都与虚拟方法有关。但是请赐教,我会相应地改变我的答案,我只被 Jon Skeet 展示过这个问题,现在他会醒来并证明我们都错了...... 据我了解,由于重写了基类中的方法并分别实现了它,他在他的后代类中有两个“相同”的方法,并且想知道为什么编译器选择了一个而不是他的电话。 @Lasse,是的,我现在明白了,你完全正确。 - 抱歉,我在你回复之前删除了我的评论。 别担心,编译器的这部分是我无论如何都不敢冒险的黑暗小巷,因为害怕被抢劫:) 非常感谢,感谢您的解释,我能够预测您的应用程序将输出什么,这与我之前的预期相反,所以我想这意味着您回答了我的问题!知道终于找到了一个很好的理由,这让我松了一口气。【参考方案2】:

请参阅 Member Lookup 和 Overload Resolution 上的 C# 语言规范部分。由于成员查找规则,派生类的覆盖方法不是候选方法,而基类方法不是基于重载解决规则的最佳匹配。

第 7.3 节

首先,构造在 T 中声明的所有可访问(第 3.5 节)成员 N 和 T 的基本类型(第 7.3.1 节)的集合。包含覆盖修饰符的声明被排除在集合之外。如果不存在名为 N 且可访问的成员,则查找不生成匹配项,并且不评估以下步骤。

第 7.4.2 节:

这些上下文中的每一个都以自己独特的方式定义候选函数成员集和参数列表,如上面列出的部分中详细描述的那样。例如,方法调用的候选集不包括标记为覆盖的方法(第 7.3 节),如果派生类中的任何方法适用,则基类中的方法不是候选方法(第 7.5 节) .5.1)。 (强调我的)

【讨论】:

这是对这个问题的最佳技术答案。【参考方案3】:

正如其他人正确指出的那样,当在一个类中选择两个适用的候选方法时,编译器总是选择最初声明“更接近”的那个检查基类层次结构时包含调用站点的类。

这似乎违反直觉。当然,如果在基类上声明了完全匹配,那么这比在派生类上声明的不完全匹配更好,是吗?

没有。选择派生较多的方法而不是派生较少的方法有两个原因。

首先是派生类的作者比基类的作者拥有更多的信息。派生类的作者对基类都了如指掌,派生类毕竟是调用者实际使用的类。如果要在调用由什么都知道的人编写的方法还是只对调用者使用的类型有所了解的人编写的方法之间进行选择时,显然优先调用派生类的设计者编写的方法是有意义的。

其次,做出这种选择会导致一种形式的脆性基类故障。我们希望保护您免受此故障的影响,因此编写了重载解决规则以尽可能避免它。

有关此规则如何保护您免受脆性基类故障的详​​细说明,请参阅我关于该主题的文章:

http://blogs.msdn.com/ericlippert/archive/2007/09/04/future-breaking-changes-part-three.aspx

有关语言处理脆性基类情况的其他方式的文章,请参阅:

http://blogs.msdn.com/b/ericlippert/archive/tags/brittle+base+classes/

【讨论】:

谢谢;看到这个决定背后的理由非常有趣。【参考方案4】:

因为这是定义语言的方式。对于virtual成员,当一个方法同时存在于基类和派生类中时,在运行时调用的实现是基于具体类型 的方法被调用的对象,而不是 声明的 类型的变量,它保存对该对象的引用。您的第一个 MyMethod 在抽象类中。所以它可以永远MyClass 类型的对象中调用 - 因为永远不会存在这样的对象。你可以实例化的是派生类MySubClass。具体类型是MySubClass,所以那个实现被调用了,不管调用它的代码在基类中。

对于非虚拟成员/方法,正好相反。

【讨论】:

抱歉,澄清一下:“第一个”MyMethod 是指第一个实现,即 MySubClass 中的第一个 MyMethod,但第二个词法实例化。让我感到困惑的是,为什么在传递 AssetDetails 时会调用最后一次出现的 MyMethod。 @kasey,是的,你是对的,我误解了你的问题......上面的lasse'答案是正确的......

以上是关于为啥这行得通?方法重载+方法覆盖+多态的主要内容,如果未能解决你的问题,请参考以下文章

JAVA基础之重载,覆盖/重写,多态

重写覆盖重载多态几个概念的区别分析

java的重写重载覆盖的差别

重写覆盖重载多态几个概念的区别分析

如果重载和覆盖相同的方法,Java中的意外多态行为

什么是java方法重载