为什么这个能够运作?方法重载+方法覆盖+多态性

12

在以下代码中:

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的子类。

当传递一个AssetDetails时,为什么第一个MyMethod调用第二个方法而不会陷入无限递归循环?


2
你的MySubClass定义上是不是忘记加上: MyClass了? - Bruno Reis
1 - 你真的想把MySubClass作为内部类吗?为什么? 2 - 你能举个例子来调用这个方法,以展示你所描述的行为吗? - Joe
MySubClass不是内部类。例如,mySubClassInstance.MyMethod(database, new AssetDetails(), ref msg); 这将调用第一个方法,然后传递给第二个方法。 - kasey
“mysubClassInstance”是如何声明的?它是基类类型还是派生类类型?(无论您将什么类型放入其中) - Lasse V. Karlsen
mysubClassInstance被声明为后代类型。 - kasey
Kasey,你运行过这个程序吗?因为当我运行时,如果在调用MySubClass时传递AssetDetails,它从不调用MySubClass的第一个方法。相反,在此调用中它会调用第二个方法(即MySubclass的最终方法)。 - Dhananjay
4个回答

10

因为调用对象的方法时,如果该对象所属的类拥有自己的实现,则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 V. Karlsen
据我理解,由于在派生类中重写了基类的方法并单独实现了它,他的派生类中有两个“相同”的方法,他想知道为什么编译器选择了其中一个而不是另一个来调用。 - Lasse V. Karlsen
@Lasse,是的,我现在明白了,你说得完全正确。抱歉,在你回复之前我删除了我的评论。 - Charles Bretana
不用担心,编译器的这一部分是黑暗的小巷,我也不敢冒险进去,怕被抢劫 :) - Lasse V. Karlsen
好的,你可以通过一些诡计在内部让我改变我的答案。 - Lasse V. Karlsen
显示剩余3条评论

5
请参阅C#语言规范中关于成员查找重载决策的部分。派生类的覆盖方法不符合成员查找规则,基类方法也不是基于重载决策规则的最佳匹配项。
第7.3节

首先,构建T及其基类型(第7.3.1节)中声明的所有名为N的可访问成员(第3.5节)的集合。排除包含override修饰符的声明。如果不存在名为N的可访问成员,则查找不会产生匹配项,以下步骤也不会被评估。

第7.4.2节:
每个上下文都以自己独特的方式定义了候选函数成员集和参数列表,详细描述在上面列出的章节中。例如,方法调用的候选集不包括标记为override的方法(第7.3节),如果派生类中的任何方法适用,则基类中的方法不是候选项(第7.5.5.1节)。

这是对这个问题最好的技术答案。 - Lasse V. Karlsen

4
正如其他人正确指出的那样,在类中存在两种适用的候选方法时,编译器总是选择最初声明“更接近”调用站点所在类的方法来检查基类层次结构。
这似乎是违反直觉的。如果在基类上声明了一个完全匹配的方法,那么这比在派生类上声明的不完全匹配的方法更好,对吗?
不是的。始终选择更具体的方法而不是更抽象的方法有两个原因。
第一个原因是派生类的作者拥有比基类的作者更多的信息。派生类的作者知道关于基类和派生类的所有内容,这毕竟是调用方实际使用的类。当需要在已知一切和仅知道调用方使用的类型之间选择调用方法时,显然优先选择由派生类的设计者编写的方法是有意义的。
其次,做出这种选择会导致脆弱的基类故障。我们希望保护您免受此故障的影响,因此编写了重载解析规则以尽可能避免它。

如需详细解释此规则如何保护您免受脆弱基类故障,请参见我关于这个主题的文章

有关其他语言处理脆弱基类情况的文章,请单击此处


谢谢,看到决定背后的理由非常有趣。 - kasey

1

这是语言定义的方式。对于虚拟成员,当基类和派生类中都存在一个方法时,在运行时调用的实现是基于调用该方法的对象的具体类型,而不是持有对该对象引用的变量的声明类型。你的第一个MyMethod在一个抽象类中。因此,它永远不能从MyClass类型的对象中调用——因为这样的对象永远不存在。你只能实例化派生类MySubClass。具体类型是MySubClass,所以调用那个实现,无论调用它的代码在基类中还是在派生类中。

对于非虚拟成员/方法,情况恰好相反。


抱歉,澄清一下:我所说的“第一个”MyMethod是指第一个实现,即在MySubClass中的第一个MyMethod,但第二个词法实例化。困扰我的是为什么传递AssetDetails时会调用MyMethod的最终出现次数。 - kasey
1
@kasey,是的,你说得对,我误解了你的问题...拉斯的回答很准确。 - Charles Bretana

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