繁体   English   中英

为什么在我的派生类中调用方法会调用基类方法?

[英]Why does calling a method in my derived class call the base class method?

考虑这个代码:

class Program
{
    static void Main(string[] args)
    {
        Person person = new Teacher();
        person.ShowInfo();
        Console.ReadLine();
    }
}

public class Person
{
    public void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}
public class Teacher : Person
{
    public new void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

当我运行此代码时,输​​出以下内容:

我是人

但是,您可以看到它是Teacher的实例,而不是Person的实例。 为什么代码会这样做?

newvirtual / override之间有区别。

您可以想象,当实例化一个类时,它只不过是一个指向其方法的实际实现的指针表。 下图可以很好地形象化此图像:

方法实现的说明

现在有不同的方法,可以定义一个方法。 与继承一起使用时,它们的行为各不相同。 标准方式始终如上图所示工作。 如果要更改此行为,可以在方法中附加不同的关键字。

1.抽象类

第一个是abstract abstract方法只是指向无处:

抽象类的插图

如果您的类包含抽象成员,则还需要将其标记为abstract ,否则编译器将不会编译您的应用程序。 您不能创建abstract类的实例,但可以从它们继承并创建继承的类的实例,并使用基类定义对其进行访问。 在您的示例中,它看起来像:

public abstract class Person
{
    public abstract void ShowInfo();
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am a teacher!");
    }
}

public class Student : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am a student!");
    }
}

如果调用, ShowInfo的行为将根据实现而有所不同:

Person person = new Teacher();
person.ShowInfo();    // Shows 'I am a teacher!'

person = new Student();
person.ShowInfo();    // Shows 'I am a student!'

StudentTeacher都是Person ,但是当要求他们提示有关自己的信息时,它们的行为会有所不同。 但是,要求他们提示信息的方法是相同的:使用Person类接口。

那么,当您继承自Person时,幕后会发生什么? 当实现ShowInfo ,指针不再指向无处 ,而是指向实际的实现! 创建Student实例时,它指向StudentShowInfo

继承方法的说明

2.虚方法

第二种方法是使用virtual方法。 除了在基类中提供可选的默认实现之外,其行为是相同的。 可以实例化具有virtual成员的类,但是继承的类可以提供不同的实现。 这是您的代码实际上应该看起来像的样子:

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am a person!");
    }
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am a teacher!");
    }
}

关键区别在于,基本成员Person.ShowInfo不再指向无处 这也是为什么您可以创建Person实例的原因(因此不再需要将其标记为abstract ):

基类内的虚拟成员的插图

您应该注意到,这看起来与现在的第一张图片没有什么不同。 这是因为virtual方法指向实现“ 标准方式 ”。 使用virtual ,您可以告诉Persons他们可以 (不是必须 )为ShowInfo提供不同的实现。 如果您提供不同的实现(使用override ),就像我对上面的Teacher所做的那样,则图像将与abstract相同。 想象一下,我们没有为Student提供定制的实现:

public class Student : Person
{
}

该代码将被这样调用:

Person person = new Teacher();
person.ShowInfo();    // Shows 'I am a teacher!'

person = new Student();
person.ShowInfo();    // Shows 'I am a person!'

Student的图像如下所示:

使用虚拟关键字的方法的默认实现的插图

3.神奇的“ new”关键字又称“ Shadowing”

new的东西更多地围绕着这个。 您可以在通用类中提供与基类/接口中的方法同名的方法。 两者都指向自己的自定义实现:

使用新关键字的“方式”插图

实现看起来像您提供的那样。 行为因您访问方法的方式而异:

Teacher teacher = new Teacher();
Person person = (Person)teacher;

teacher.ShowInfo();    // Prints 'I am a teacher!'
person.ShowInfo();     // Prints 'I am a person!'

可能需要这种行为,但在您的情况下却具有误导性。

我希望这会使您更容易理解!

C#中的子类型多态性使用显式虚拟性,类似于C ++,但不同于Java。 这意味着您必须明确地将方法标记为可重写(即virtual )。 在C#中,您还必须将覆盖方法显式标记为覆盖(例如, override ),以防止输入错误。

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

在您问题的代码中,您使用new ,它执行阴影而不是覆盖。 阴影只会影响编译时的语义,而不会影响运行时的语义,因此会产生意想不到的输出。

为了调用放在父类引用中的类对象的方法,必须使方法虚拟化,并且必须重写子类中的函数。

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}
public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

虚拟方法

调用虚拟方法时,将检查对象的运行时类型是否有重写成员。 如果没有派生类重写该成员,则将调用最派生类中的重写成员,该成员可能是原始成员。 默认情况下,方法是非虚拟的。 您不能覆盖非虚拟方法。 您不能将虚拟修饰符与静态,抽象,私有或替代修饰符MSDN一起使用

使用新的阴影

您使用的是新关键字而不是替代关键字,这就是new所做的

  • 如果派生类中的方法前面没有new或override关键字,则编译器将发出警告,并且该方法的行为就像存在new关键字一样。

  • 如果派生类中方法前面带有new关键字,则该方法被定义为独立于基类中的方法 ,此MSDN文章对此进行了很好的解释。

早期绑定与后期绑定

我们在编译时就对普通方法(非虚拟方法)进行了早期绑定,这是当前情况, 编译器会将调用绑定到基类的方法上,该基类是引用类型的方法(基类),而不是将对象保存在基类的引用中类,即派生类对象 这是因为ShowInfo不是虚拟方法。 后期绑定是在运行时使用虚拟方法表 (vtable)对(虚拟/重写方法)执行的。

对于正常功能,编译器可以计算出它在内存中的数字位置。 然后,当调用该函数时,它可以生成一条指令以在该地址处调用该函数。

对于具有任何虚拟方法的对象,编译器将生成一个v表。 本质上,这是一个包含虚拟方法地址的数组。 每个具有虚拟方法的对象都将包含由编译器生成的隐藏成员,即v表的地址。 调用虚拟函数时,编译器将确定v表中适当方法的位置。 然后,它将生成代码以查看对象v表并在此位置调​​用虚拟方法Reference

我想以阿赫拉特的答案为基础 为了完整起见,不同之处在于OP希望派生类的方法中的new关键字覆盖基类方法。 它实际上所做的是隐藏基类方法。

在C#中,作为另一个答案,传统方法的覆盖必须是明确的。 必须将基类方法标记为virtual ,并且派生类必须专门override基类方法。 如果这样做,则将对象视为基类还是派生类的实例都没有关系。 找到并调用派生的方法。 这是用与C ++类似的方式完成的。 标记为“虚拟”或“覆盖”的方法在编译时通过确定引用对象的实际类型,并沿树从变量类型向下遍历到对象实际类型,从而在运行时“延迟”解析,查找由变量类型定义的方法的最派生实现。

这不同于Java,后者允许“隐式覆盖”。 对于实例方法(非静态),仅定义具有相同签名(名称和参数的数量/类型)的方法将导致子类覆盖超类。

由于扩展或覆盖不受您控制的非虚拟方法的功能通常很有用,因此C#还包括new上下文关键字。 new关键字“隐藏”父方法,而不是覆盖它。 无论是否是虚拟的,任何可继承的方法都可以隐藏。 这样,开发人员可以利用您想要从父级继承的成员,而不必解决那些您不需要的成员,同时仍然允许您向​​代码使用者提供相同的“接口”。

隐藏的作用与从使用您的对象的人的角度来看,在定义隐藏方法的继承级别或更低级别的继承类似。 从问题的示例中,一个编码器创建了一个Teacher并将该引用存储在Teacher类型的变量中,这将对Teacher看到ShowInfo()实现的行为,而该行为对Person隐藏了。 但是,某人在一个Person记录集合中使用您的对象(就像您一样)将看到ShowInfo()的Person实现的行为。 因为Teacher的方法不会覆盖其父类(这也要求Person.ShowInfo()是虚拟的),所以在Person的抽象级别工作的代码不会找到Teacher的实现,也不会使用它。

此外, new关键字不仅会显式地执行此操作,而且C#允许隐式方法隐藏。 只需定义一个与父类方法具有相同签名的方法,而无需overridenew将其隐藏(尽管它会产生编译器警告或某些重构助手(如ReSharper或CodeRush)的抱怨)。 这是C#的设计者在C ++的显式覆盖与Java的隐式覆盖之间做出的折衷,尽管它很优雅,但是如果您来自任何一种较旧的语言,它都不会总是产生您期望的行为。

这是新内容:当您将两个关键字组合在长的继承链中时,这将变得很复杂。 考虑以下:

class Foo { public virtual void DoFoo() { Console.WriteLine("Foo"); } }
class Bar:Foo { public override sealed void DoFoo() { Console.WriteLine("Bar"); } }
class Baz:Bar { public virtual void DoFoo() { Console.WriteLine("Baz"); } }
class Bai:Baz { public override void DoFoo() { Console.WriteLine("Bai"); } }
class Bat:Bai { public new void DoFoo() { Console.WriteLine("Bat"); } }
class Bak:Bat { }

Foo foo = new Foo();
Bar bar = new Bar();
Baz baz = new Baz();
Bai bai = new Bai();
Bat bat = new Bat();

foo.DoFoo();
bar.DoFoo();
baz.DoFoo();
bai.DoFoo();
bat.DoFoo();

Console.WriteLine("---");

Foo foo2 = bar;
Bar bar2 = baz;
Baz baz2 = bai;
Bai bai2 = bat;
Bat bat2 = new Bak();

foo2.DoFoo();
bar2.DoFoo();
baz2.DoFoo();
bai2.DoFoo();    

Console.WriteLine("---");

Foo foo3 = bak;
Bar bar3 = bak;
Baz baz3 = bak;
Bai bai3 = bak;
Bat bat3 = bak;

foo3.DoFoo();
bar3.DoFoo();
baz3.DoFoo();
bai3.DoFoo();    
bat3.DoFoo();

输出:

Foo
Bar
Baz
Bai
Bat
---
Bar
Bar
Bai
Bai
Bat
---
Bar
Bar
Bai
Bai
Bat

预计第一批五套; 因为每个级别都有一个实现,并且被引用为与实例化类型相同的对象,所以运行时将每个调用解析为变量类型引用的继承级别。

第二组五个是将每个实例分配给直接父类型的变量的结果。 现在,一些行为上的差异消失了。 foo2实际上是转换为FooBar ,仍然会找到实际对象类型Bar的更多派生方法。 bar2是一个Baz ,但是与foo2不同,因为Baz不会显式覆盖Bar的实现(它不能; Bar对其进行了sealed ),因此在运行时查看“自上而下”时不会看到它,因此调用Bar的实现。 注意,Baz不必使用new关键字。 如果省略关键字,则会收到编译器警告,但是C#中的隐含行为是隐藏父方法。 baz2Bai ,它重写了Baznew实现,因此其行为类似于foo2的行为。 称为Bai的实际对象类型的实现。 bai2是一个Bat ,它再次隐藏了其父Bai的方法实现,即使Bai的实现未密封,它的行为也与bar2相同,因此从理论上说Bat可以重写而不是隐藏该方法。 最后, bat2是一个Bak ,没有任何一种压倒性的实现,仅使用其父代的实现。

第三组(每组五个)说明了完整的自上而下的分辨率行为。 实际上,所有内容都引用链中最派生类的实例Bak ,但是通过从继承链的该级别开始并向下钻取到该方法的最派生的显式覆盖,可以执行每个变量类型级别的解析。是BarBaiBat中的那些。 因此,方法隐藏“打破”了整个继承链; 您必须使用隐藏该方法的继承级别或更低级别的对象,才能使用隐藏方法。 否则, 隐藏的方法将被“发现”并使用。

请阅读有关C#中的多态多态(C#编程指南)

这是一个例子:

使用new关键字时,将调用新的类成员,而不是已替换的基类成员。 这些基类成员称为隐藏成员。 如果将派生类的实例强制转换为基类的实例,则仍可以调用隐藏的类成员。 例如:

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Calls the old method.

您需要将其virtual ,然后在Teacher覆盖该功能。 在继承并使用基指针引用派生类时,需要使用virtual重写它。 new用于在派生类引用而不是base类引用上隐藏base类方法。

我想添加更多示例,以扩展有关此方面的信息。 希望这也会有所帮助:

这是一个代码示例,该示例清除了将派生类型分配给基本类型时发生的事情。 在这种情况下,哪些方法可用以及覆盖方法和隐藏方法之间的区别。

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            a.foo();        // A.foo()
            a.foo2();       // A.foo2()

            a = new B();    
            a.foo();        // B.foo()
            a.foo2();       // A.foo2()
            //a.novel() is not available here

            a = new C();
            a.foo();        // C.foo()
            a.foo2();       // A.foo2()

            B b1 = (B)a;    
            b1.foo();       // C.foo()
            b1.foo2();      // B.foo2()
            b1.novel();     // B.novel()

            Console.ReadLine();
        }
    }


    class A
    {
        public virtual void foo()
        {
            Console.WriteLine("A.foo()");
        }

        public void foo2()
        {
            Console.WriteLine("A.foo2()");
        }
    }

    class B : A
    {
        public override void foo()
        {
            // This is an override
            Console.WriteLine("B.foo()");
        }

        public new void foo2()      // Using the 'new' keyword doesn't make a difference
        {
            Console.WriteLine("B.foo2()");
        }

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

    class C : B
    {
        public override void foo()
        {
            Console.WriteLine("C.foo()");
        }

        public new void foo2()
        {
            Console.WriteLine("C.foo2()");
        }
    }
}

对于以下代码行,另一个小异常是:

A a = new B();    
a.foo(); 

VS编译器(intellisense)将a.foo()显示为A.foo()。

因此,很明显,当将更多派生类型分配给基本类型时,“基本类型”变量将充当基本类型,直到引用了派生类型中覆盖的方法为止。 对于隐藏的方法或在父类型和子类型之间具有相同名称(但不被覆盖)的方法,这可能会有点违反直觉。

此代码示例应有助于描述这些警告!

C#与Java在父/子类重写行为方面有所不同。 在Java中,默认情况下所有方法都是虚拟的,因此开箱即用地支持所需的行为。

在C#中,您必须在基类中将方法标记为虚方法,然后您将获得所需的内容。

new关键字表明,仅当您将教师类的实例存储在类型为Teacher的变量中时,当前类中的方法才有效。 或者您可以使用强制转换触发它:((Teacher)Person).ShowInfo()

这里的变量“老师”的类型是typeof(Person) ,这种类型对Teacher类不了解,也不试图在派生类型中查找任何方法。 要调用Teacher类的方法,您应该(person as Teacher).ShowInfo()变量:( (person as Teacher).ShowInfo()

要基于值类型调用特定的方法,应在基类中使用关键字“ virtual”,并在派生类中覆盖虚拟方法。 这种方法可以实现带有或不具有覆盖虚拟方法的派生类。 对于没有过多虚数的类型,将调用基类的方法。

public class Program
{
    private static void Main(string[] args)
    {
        Person teacher = new Teacher();
        teacher.ShowInfo();

        Person incognito = new IncognitoPerson ();
        incognito.ShowInfo();

        Console.ReadLine();
    }
}

public class Person
{
    public virtual void ShowInfo()
    {
        Console.WriteLine("I am Person");
    }
}

public class Teacher : Person
{
    public override void ShowInfo()
    {
        Console.WriteLine("I am Teacher");
    }
}

public class IncognitoPerson : Person
{

}

可能为时已晚...但是问题很简单,答案应该具有相同的复杂度。

在您的代码变量中,人对Teacher.ShowInfo()一无所知。 无法从基类引用中调用last方法,因为它不是虚拟的。

有一种有用的继承方法-尝试想象您想对代码层次结构说些什么。 还要尝试想象一个或另一种工具对自己的看法。 例如,如果您将虚函数添加到基类中,您应该:1.它可以具有默认实现; 2.可以在派生类中重新实现。 如果添加抽象函数,则仅意味着一件事-子类必须创建一个实现。 但是,如果您具有普通功能-您不要期望任何人更改其实现。

编译器之所以这样做是因为它不知道它是Teacher 它所知道的只是它是一个Person或从它派生而来的东西。 因此,它所能做的就是调用Person.ShowInfo()方法。

只是想给出一个简短的答案-

您应该在可能被覆盖的类中使用virtualoverride 使用virtual表示可以被子类override的方法,并将override为应该覆盖此类virtual方法的方法。

除了一些更改外,我用Java编写了与您上面提到的代码相同的代码,但除此以外,它均能正常工作。 基类的方法被覆盖,因此显示的输出为“我是老师”。

原因:当我们创建基类的引用(该类能够具有派生类的引用实例)时,该基类实际上包含派生类的引用。 而且我们知道,实例总是先在其方法中查找其方法,然后再执行它,而在实例中找不到定义的情况下,它会在层次结构中向上移动。

public class inheritance{

    public static void main(String[] args){

        Person person = new Teacher();
        person.ShowInfo();
    }
}

class Person{

    public void ShowInfo(){
        System.out.println("I am Person");
    }
}

class Teacher extends Person{

    public void ShowInfo(){
        System.out.println("I am Teacher");
    }
}

基于Keith S.的出色演示和其他每个人的质量回答,并且为了更加完整,我们继续前进,并抛弃显式的接口实现以演示其工作原理。 考虑以下内容:

名称空间LinqConsoleApp {

class Program
{

    static void Main(string[] args)
    {


        Person person = new Teacher();
        Console.Write(GetMemberName(() => person) + ": ");
        person.ShowInfo();

        Teacher teacher = new Teacher();
        Console.Write(GetMemberName(() => teacher) + ": ");
        teacher.ShowInfo();

        IPerson person1 = new Teacher();
        Console.Write(GetMemberName(() => person1) + ": ");
        person1.ShowInfo();

        IPerson person2 = (IPerson)teacher;
        Console.Write(GetMemberName(() => person2) + ": ");
        person2.ShowInfo();

        Teacher teacher1 = (Teacher)person1;
        Console.Write(GetMemberName(() => teacher1) + ": ");
        teacher1.ShowInfo();

        Person person4 = new Person();
        Console.Write(GetMemberName(() => person4) + ": ");
        person4.ShowInfo();

        IPerson person3 = new Person();
        Console.Write(GetMemberName(() => person3) + ": ");
        person3.ShowInfo();

        Console.WriteLine();

        Console.ReadLine();

    }

    private static string GetMemberName<T>(Expression<Func<T>> memberExpression)
    {
        MemberExpression expressionBody = (MemberExpression)memberExpression.Body;
        return expressionBody.Member.Name;
    }

}
interface IPerson
{
    void ShowInfo();
}
public class Person : IPerson
{
    public void ShowInfo()
    {
        Console.WriteLine("I am Person == " + this.GetType());
    }
    void IPerson.ShowInfo()
    {
        Console.WriteLine("I am interface Person == " + this.GetType());
    }
}
public class Teacher : Person, IPerson
{
    public void ShowInfo()
    {
        Console.WriteLine("I am Teacher == " + this.GetType());
    }
}

}

这是输出:

人员:我是人员== LinqConsoleApp.Teacher

老师:我是老师== LinqConsoleApp.Teacher

person1:我是老师== LinqConsoleApp.Teacher

person2:我是老师== LinqConsoleApp.Teacher

老师1:我是老师== LinqConsoleApp.Teacher

person4:我是Person == LinqConsoleApp.Person

person3:我是接口Person == LinqConsoleApp.Person

需要注意的两件事:
Teacher.ShowInfo()方法忽略了new关键字。 省略new时,方法的行为与显式定义new关键字的行为相同。

您只能将override关键字与虚拟关键字一起使用。 基类方法必须是虚拟的。 或抽象,在这种情况下,该类也必须是抽象的。

person获得ShowInfo的基本实现,因为Teacher类不能覆盖基本实现(无虚拟声明),并且person是.GetType(Teacher),因此它隐藏了Teacher类的实现。

Teacher之所以获得ShowInfo的派生Teacher的实现,是因为Teacher是Typeof(Teacher),而不是在Person继承级别上。

person1获得派生的Teacher实现,因为它是.GetType(Teacher),并且隐含的new关键字隐藏了基本实现。

即使person2确实实现了IPerson并获得了对IPerson的显式转换,它也获得了派生的Teacher实现。 再次这是因为Teacher类没有显式实现IPerson.ShowInfo()方法。

Teacher1还获得了派生的Teacher实现,因为它是.GetType(Teacher)。

因为只有Person类显式实现了该方法,并且person3是IPerson类型的实例,所以只有person3获得了ShowInfo的IPerson实现。

为了显式实现接口,您必须声明目标接口类型的var实例,并且类必须显式实现(完全限定)接口成员。

请注意,甚至person4都没有获得IPerson.ShowInfo实现。 这是因为即使person4是.GetType(Person)并且即使Person实现IPerson,person4也不是IPerson的实例。

LinQPad示例可盲目启动并减少代码重复,我认为这是您正在尝试做的事情。

void Main()
{
    IEngineAction Test1 = new Test1Action();
    IEngineAction Test2 = new Test2Action();
    Test1.Execute("Test1");
    Test2.Execute("Test2");
}

public interface IEngineAction
{
    void Execute(string Parameter);
}

public abstract class EngineAction : IEngineAction
{
    protected abstract void PerformAction();
    protected string ForChildren;
    public void Execute(string Parameter)
    {  // Pretend this method encapsulates a 
       // lot of code you don't want to duplicate 
      ForChildren = Parameter;
      PerformAction();
    }
}

public class Test1Action : EngineAction
{
    protected override void PerformAction()
    {
        ("Performed: " + ForChildren).Dump();
    }
}

public class Test2Action : EngineAction
{
    protected override void PerformAction()
    {
        ("Actioned: " + ForChildren).Dump();
    }
}
    class Program
    {
        static void Main(string[] args)
        { 
            AA aa = new CC();
            aa.Print();                      
        }
    }
    
    public class AA {public virtual void Print() => WriteLine("AA");}
    public class BB : AA {public override void Print() => WriteLine("BB");}
    public class DD : BB {public override void Print() => WriteLine("DD");}
    public class CC : DD {new public void Print() => WriteLine("CC");}
OutPut - DD

对于那些想知道 CLR 如何在内部调用 C# 中的 new 和 virtual 方法的人。

当使用 new 关键字时,为CC.Print()分配了新的内存槽,并且它不会覆盖基类内存槽,因为派生类前面有 new 关键字,该方法被定义为独立的基类中的方法

当使用覆盖时,内存插槽被派生类成员覆盖,在这种情况下,由AA.Print()插槽覆盖BB.Print() BB.Print()是超驰由DD.Print() 当我们调用AA aa = new CC() 编译器将为CC.Print()创建新的内存槽,但是当它转换为 AA 时,然后根据 Vtable Map,调用 AA 可覆盖对象 DD。

参考 - c# - 覆盖和隐藏之间的确切区别 - 堆栈溢出.NET Framework 内部:CLR 如何创建运行时对象 | 微软文档

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM