.Net继承和方法重载

本文关键字:重载 方法 继承 Net | 更新日期: 2023-09-27 18:25:06

下面是一个代码示例:

class Program
{
    static void Main(string[] args)
    {
        var obj = new DerivedClass();
        obj.SomeMethod(5);
    }
}
class BaseClass
{
    internal void SomeMethod(int a) { }
}
class DerivedClass : BaseClass
{
    internal void SomeMethod(long a) { }
}

有人能解释一下为什么派生类中的方法被称为(而不是基类方法)吗?我需要对这种情况作出详细解释。如果有任何有用的文章链接,我将不胜感激。

谢谢。

.Net继承和方法重载

准确的措辞和位置因规范的不同版本而异,但例如,这里可以阅读:

构建了方法调用的候选方法集。从之前的成员查找(§7.3)中找到的与M相关的一组方法开始,该集合被缩减为适用于参数列表a的方法。集合缩减包括将以下规则应用于集合中的每个方法T.N,其中T是声明方法N的类型:

如果N不适用于A(§7.4.2.1),则从集合中删除N。

如果N适用于A(§7.4.2.1),则在T的基类型中声明的所有方法都将从集合中删除。

因此,假设我们有类型为DerivedClassobj,则成员方法集包含来自DerivedClassvoid SomeMethod(long)和来自BaseClassvoid SomeMethod(int)

这两种方法都适用,事实上void SomeMethod(int)是一个更好的重载匹配,但由于上面引用的最后一句中的规则,一旦发现void SomeMethod(long)适用,则从候选集合中删除基类中的所有方法,这意味着不再考虑void SomeMethod(int)

好吧,这就是规格方面的技术原因。首先,规格中的设计原因是什么?

好吧,想象一下BaseClass一开始被定义为:

public class BaseClass
{
}

如果其余的代码是相同的,那么很明显,对obj.SomeMethod(5)的调用应该调用唯一存在的同名方法。

现在考虑一下,如果编写代码之后,方法void SomeMethod(int)被添加到BaseClass中。事实上,考虑到这可能与DerivedClass不同,并且由单独的作者编写。

现在,对SomeMethod()的调用的含义发生了变化。更糟糕的是,它的更改与否取决于给定机器已经应用或尚未应用的更新。(更糟糕的是,由于C#重载解析中没有使用返回类型,因此它的更改方式可能会在已编译的代码中产生编译错误:这是一个完全破坏性的更改)。

如果派生程度更高的类中存在重载候选者,则排除基类中定义的方法的规则允许在未来发生更改时更大程度地确保调用的是要调用的方法。(当然,如果你打算调用基类方法,你可能会感到惊讶,但在编码时,你可以发现这个问题,并使用强制转换来确保结果是你想要的行为)。

这可能会让一些人感到惊讶的结果是:

class Program
{
    static void Main(string[] args)
    {
        var obj = new DerivedClass();
        obj.SomeMethod(5);
    }
}
class BaseClass
{
    public virtual void SomeMethod(int a) { Console.WriteLine("Base"); }
}
class DerivedClass : BaseClass
{
    public override void SomeMethod(int a) { Console.WriteLine("Defined in Base, overriden in Derived"); }
    public void SomeMethod(long a) { Console.WriteLine("Derived"); }
}

这将输出Derived,因为此规则根据方法的声明位置应用,即使存在来自重写的实现。

(该规则如此工作的另一个原因是,当它被转换为CIL时,调用将包含关于它在其中声明的类的信息。这里的规则是最简单的方法。也就是说;1)在CIL的设计中应用了类似的逻辑,2)以上使其成为CIL的一个特性,供C#人员使用,而不是反对)。

var obj = new DerivedClass();

var关键字只是C#中的句法糖;这基本上与相同

DerivedClass obj = new DerivedClass();

因此,你称之为DerrivedClass.SomeMethod,这正是你正在经历的行为。如果你这样定义你的变量,你会看到不同:

BaseClass obj = new DerivedClass();

评论后编辑: 没错,我可能没有正确回答确切的问题,所以让我现在试试:

原始代码中的方法调用与两个方法(在基类和派生类中)的签名匹配,因为在这种情况下,参数5可以是intlong。然而,基本方法没有标记为virtual(这将允许重写),并且"派生"方法也不是真正派生的,因为它没有标记为override

但是,请注意,即使确实将其标记为override,也会出现错误,因为实际上,这两个方法签名是不等价的:一个采用int,而另一个采用long类型。这将导致编译时错误,并显示消息:"没有找到合适的方法来覆盖"。

如果你阅读下面我原始答案的其余部分,希望其余部分会变得清晰起来。


原始答案:

这里有几点需要注意:

1) 你的方法有不同的特征;一个需要长时间,另一个需要int

2) 您尚未将方法标记为virtualoverride

经过编辑的代码版本加上一些注释可能会更清楚地说明这些东西是如何工作的:

internal class Program
{
    private static void Main(string[] args)
    {
        var obj = new DerivedClass();
        // That is the same as:
        //DerivedClass obj = new DerivedClass();
        // Will call the base method, since that now matches the
        // signature (takes an int parameter). DerivedClass simply
        // does not HAVE a method with that signature on it's own:
        obj.SomeMethod(5); // will output "base with int"
        // Now call the other method, which IS defined in DerivedClass, 
        // by appending an "l", to mark this as a Long:
        obj.SomeMethod(5l); // Will output "derived"
        // This would call the base method directly
        var obj2 = new BaseClass();
        obj2.SomeMethod(5l); 
        Console.ReadKey();
    }
}
internal class BaseClass
{
    internal void SomeMethod(int a)
    {
        Console.WriteLine("base with int");
    }
    // Added method for the example:
    // Note that "virtual" allows it to be overridden
    internal virtual void SomeMethod(long a)
    {
        Console.WriteLine("base with long");
    }
}
internal class DerivedClass : BaseClass
{
    // Note: Overrides the base method now
    internal override void SomeMethod(long a)
    {
        Console.WriteLine("derived");
    }
}

来自C#语言参考:

7.5.5功能成员调用

本节描述在运行时发生的过程调用特定的函数成员。假设绑定时间进程已经确定了要调用的特定成员,可能通过将过载分辨率应用于一组候选功能成员。

为了描述调用过程,函数成员分为两类:

  • 静态函数成员。<snip>
  • 实例函数成员。这些是实例方法、实例属性访问器和索引器访问器。实例函数成员是非虚拟的或虚拟的,并且总是在特定实例。实例由实例计算表达式,并且它可以在函数成员中作为this(§7.6.7)。函数成员调用的运行时处理其中M是功能成员,如果M是实例成员,则E是实例表达式:
    • 如果M是静态函数成员:<snip>
    • 如果M是在值类型中声明的实例函数成员:<snip>
    • 如果M是在引用类型中声明的实例函数成员:
      • E进行评估。如果此评估导致异常,则不执行进一步的步骤
      • 根据§7.5.1中的描述对论证列表进行评估
      • 如果E的类型是值类型,则<snip>
      • 检查E的值是否有效。如果E的值为null,则抛出System.NullReferenceException,并且不再执行任何步骤执行
      • 确定要调用的函数成员实现:
        • 如果E的绑定时间类型是接口,则<snip>
        • 否则,如果M是虚拟函数成员,则<snip>
        • 否则,M是非虚拟函数成员,要调用的函数成员是M本身
      • 调用在上述步骤中确定的函数成员实现。E引用的对象变为引用的对象通过这个

此外,在1.6.6.4虚拟、覆盖和抽象方法中,我们有

调用虚拟方法时,实例的运行时类型为其进行调用决定了实际的方法要调用的实现。在非虚拟方法调用中,实例的编译时类型是决定因素

因此,当编译代码时,所使用的变量的类型决定了调用什么方法。

public class A { public void WhoAreYou() { Console.WriteLine("A"); } }
public class B : A  { public void WhoAreYou() { Console.WriteLine("B"); } }
internal class Program
{
private static void Main(string[] args)
{
    (new B() as A).WhoAreYou(); // "A"
    (new B()).WhoAreYou(); // "B"
    Console.ReadLine();
}

请注意,编译器会警告您潜在的问题,因为将被调用的方法因用于定义类实例的类型而异。

我所理解的是,由于没有应用覆盖/隐藏,派生类的方法在main()中实例化时被调用。

在方法重写中:指向子类对象的基类引用变量将调用子类中的重写方法。派生类方法签名中使用了"Override"关键字。

在方法隐藏中:指向子类对象的基类引用变量将调用基类中的隐藏方法。派生类方法签名中使用了"New"关键字。