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

时间:2013-07-18 07:45:48

标签: c# class derived-class

考虑以下代码:

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的实例。代码为什么这样做?

15 个答案:

答案 0 :(得分:363)

newvirtual / override之间存在差异。

你可以想象,一个类在实例化时只不过是一个指针表,指向其方法的实际实现。下面的图像应该很好地可视化:

Illustration of method implementations

现在有不同的方法,可以定义一种方法。当它与继承一起使用时,每个行为都不同。标准方式总是像上图所示。如果要更改此行为,可以在方法中附加不同的关键字。

1。抽象类

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

Illustration of abstract classes

如果您的类包含抽象成员,则还需要将其标记为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实例时,它指向Student s ShowInfo

Illustration of inherited methods

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)的原因:

Illustration of a virtual member inside a base class

你应该注意到,这与现在的第一张图片没有什么不同。这是因为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的图片看起来像这样:

Illustration of the default implementation of a method, using virtual-keyword

3。神奇的“新”关键字又名“阴影”

new更像是一个黑客。您可以在通用类中提供方法,这些方法与基类/接口中的方法具有相同的名称。两者都指向自己的自定义实现:

Illustration of the "way around" using the new-keyword

实现看起来像你提供的那个。根据您访问方法的方式,行为会有所不同:

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

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

可能需要此行为,但在您的情况下,这会产生误导。

我希望这能让你更清楚地了解它!

答案 1 :(得分:45)

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 {{1}}而不是覆盖。阴影仅影响编译时语义而不是运行时语义,因此会影响意外输出。

答案 2 :(得分:25)

你必须创建方法virtual并且必须覆盖子类中的函数,以便调用放在父类引用中的类对象的方法。

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");
    }
}

虚拟方法

  

调用虚方法时,对象的运行时类型为   检查一个压倒一切的成员。最重要的成员   调用派生类,如果不是,则可以是原始成员   派生类已重写成员。默认情况下,方法是   非虚。您无法覆盖非虚方法。你不能使用   virtual修饰符,包含static,abstract,private或override   修饰符,MSDN

使用New进行遮蔽

您正在使用新关键字而不是覆盖,这就是新功能

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

  • 如果派生类中的方法前面带有new关键字,则该方法被定义为独立于基类中的方法,此MSDN article解释它非常好。

早期结合VS晚期结合

我们在编译时对正常方法(非虚拟)进行早期绑定,这是编译器将绑定调用基类方法(即引用类型的方法(基类)而不是对象)的典型情况。在基类的引用中保存,即派生类对象。这是因为ShowInfo不是虚方法。使用virtual method table(vtable)在运行时为(虚拟/重写方法)执行后期绑定。

  

对于普通函数,编译器可以计算出数字位置   它在记忆中。然后在调用函数时它可以生成   在此地址调用该函数的指令。

     

对于具有任何虚方法的对象,编译器将生成   一张v表。这本质上是一个包含地址的数组   虚拟方法。每个拥有虚方法的对象都会   包含由编译器生成的隐藏成员,即地址   的表格。当调用虚函数时,编译器会   弄清楚该位置的适当方法是什么   v表。然后它将生成代码以查看对象v-table和   在此位置调​​用虚拟方法Reference

答案 3 :(得分:7)

我想建立Achratt's answer。为了完整起见,不同之处在于OP期望派生类的方法中的new关键字覆盖基类方法。它实际上做的是隐藏基类方法。

在C#中,作为另一个提到的答案,传统的方法覆盖必须是明确的;基类方法必须标记为virtual,派生类必须具体override基类方法。如果这样做,那么对象是否被视为基类或派生类的实例并不重要;找到并调用派生方法。这是以与C ++类似的方式完成的;标记为"虚拟"的方法或"覆盖",在编译时,已解决"迟到" (在运行时)通过确定引用对象的实际类型,并沿着树向下遍历对象层次结构从变量类型到实际对象类型,以找到由变量类型定义的方法的最派生实现。

这与Java不同,它允许"隐式覆盖&#34 ;;例如方法(非静态),简单地定义相同签名的方法(名称和数量/参数类型)将导致子类覆盖超类。

因为扩展或覆盖您无法控制的非虚拟方法的功能通常很有用,所以C#还包含new上下文关键字。 new关键字"隐藏"父方法而不是覆盖它。任何可继承的方法都可以被隐藏,无论它是否是虚拟的;这允许您(开发人员)利用您想要从父母那里继承的成员,而不必解决您不熟悉的成员,同时仍然允许您呈现相同的界面"对你的代码的消费者。

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

此外,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,实际上是Bar强制转换为Foo,仍会找到实际对象类型Bar的派生方法。 bar2Baz,但与foo2不同,因为Baz没有明确覆盖Bar的实现(它可以&#t; t; Bar {{1}在查看"自上而下"时,运行时没有看到它,因此调用了Bar的实现。请注意,Baz不必使用sealed关键字;如果省略关键字,您将收到编译器警告,但C#中的隐含行为是隐藏父方法。 newbaz2,它会覆盖Bai的{​​{1}}实施,因此其行为类似于Baz&n;实际的对象类型在Bai中的实现被称为。 newfoo2,它再次隐藏了其父bai2方法实现,即使执行了Bai,它的行为也与Bat相同没有密封,所以理论上Bat可以覆盖而不是隐藏方法。最后,Baibar2,它没有任何类型的重写实现,只是使用其父类的实现。

第三组五个图解了完整的自上而下的分辨率行为。实际上,所有内容都是引用链中bat2中派生类最多的类的实例,但是在每个级别的变量类型中的解析都是通过从继承链的该级别开始并向下钻取到最多派生的来执行的。显式覆盖方法,即BakBakBar中的方法。因此隐藏的方法"中断"最重要的继承链;您必须使用隐藏方法的继承级别或更低级别的对象,以便使用隐藏方法。否则,隐藏方法是"未覆盖"并改为使用。

答案 4 :(得分:4)

请阅读C#中的多态性:Polymorphism (C# Programming Guide)

这是一个例子:

  

使用new关键字时,将调用新的类成员   被替换的基类成员的数量。那些基类   成员被称为隐藏成员。隐藏的班级成员仍然可以   如果派生类的实例被强制转换为实例,则调用   基类。例如:

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

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

答案 5 :(得分:3)

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

答案 6 :(得分:3)

我想补充几个例子来扩展这方面的信息。希望这也有帮助:

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

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()。

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

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

答案 7 :(得分:2)

C#与父/子类覆盖行为中的java不同。默认情况下,Java中的所有方法都是虚拟的,因此您需要的行为是开箱即用的。

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

答案 8 :(得分:2)

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

答案 9 :(得分:1)

此处变量'teacher'的类型为typeof(Person),此类型对Teacher类没有任何了解,也不会尝试在派生类型中查找任何方法。要调用Teacher类的方法,您应该转换变量:(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
{

}

答案 10 :(得分:1)

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

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

有一种有用的继承方法 - 试着想象你想用你的代码层次结构说些什么。还试着想象一个或另一个工具对自己的看法。例如。如果你将虚函数添加到基类中,你会想:1。它可以有默认实现; 2.它可能在派生类中重新实现。如果添加抽象函数,则只表示一件事 - 子类必须创建实现。但是如果你有简单的功能 - 你不希望任何人改变它的实现。

答案 11 :(得分:0)

编译器执行此操作是因为它不知道它是Teacher。它只知道它是Person或从它衍生的东西。所以它所能做的就是调用Person.ShowInfo()方法。

答案 12 :(得分:0)

只想简单回答一下 -

您应该在可以覆盖的类中使用virtualoverride。对于可以由子类覆盖的方法使用virtual,对于应该覆盖此类override方法的方法使用virtual

答案 13 :(得分:0)

我在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");
    }
}

答案 14 :(得分:0)

基于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());
    }
}

}

这是输出:

person:我是Person == 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类的实现。

老师获取ShowInfo的派生教师实现,因为它是Typeof(教师),而不是在Person继承级别。

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

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

teacher1也获取派生的Teacher实现,因为它是.GetType(Teacher)。

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

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

请注意,即使person4获得了IPerson.ShowInfo实现。这是因为即使person4是.GetType(Person),即使Person实现了IPerson,person4也不是IPerson的实例。