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


146

考虑以下代码:

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


3
来自Java人士的问题:是Console.ReadLine();吗?这个例子有必要吗?
Rich

2
@Shahrooz我无法回答您的问题-我不知道C#。我在问一个非常琐碎的C#问题,即是否有必要在main方法中调用ReadLine以便能够在Person和Teacher类中调用WriteLine。
Rich

6
是的,.Net在Main()退出时自动关闭控制台窗口。为了解决这个问题,我们使用Console.Read()或Console.Readline()来等待其他输入,以便控制台保持打开状态。
肯帕奇船长

15
@Rich不,不是必须的,但是由于这个原因您经常会看到它:从Visual Studio运行控制台程序时,在程序终止时命令窗口会立即关闭,因此,如果您想查看程序输出,则需要告诉它等待。
AakashM

1
@AakashM谢谢-我花时间在Eclipse控制台是Eclipse窗口的一部分的Eclipse中,所以它没有关闭。那是很合理的。
Rich

Answers:


368

newvirtual/ 之间有区别override

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

方法实现的说明

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

1.抽象类

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

抽象类的插图

如果您的类包含抽象成员,则还需要将其标记为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!'

这两个,StudentS和Teachers为PersonS,但他们的行为不同,当他们被要求对自己的提示信息。但是,要求他们提示信息的方法是相同的:使用Person类接口。

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

继承方法的说明

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。想象一下,我们没有为Students 提供自定义实现:

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!'

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

我希望这可以使您更清楚地理解!


9
感谢您的出色回答

6
您用什么来生成这些图?
BlueRaja-Danny Pflughoeft13年

2
优秀且非常彻底的答案。
Nik Bougalis

8
tl; dr用来new破坏函数的继承并使新函数与超类的函数分离
棘手怪胎

3
@Taymon:其实不是...我只是想澄清一下,现在电话打了Person,不是Student;)
卡斯顿

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,这确实阴影,而不是覆盖。阴影只会影响编译时的语义,而不会影响运行时的语义,因此会产生意想不到的输出。


4
谁说OP知道那些意思。
科尔·约翰逊

@ColeJohnson我将添加一个说明。

25

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

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处调用虚拟方法。


7

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

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

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

因为扩展或覆盖不受您控制的非虚拟方法的功能通常很有用,所以C#还包括newcontextual关键字。所述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,实际上是转换BarFoo,仍会找到实际对象类型Bar的更多派生方法。bar2是一个Baz,但是与有所不同foo2,因为Baz不会显式覆盖Bar的实现(它不能; Bar否sealed),因此在运行时查看“自上而下”时不会看到它,因此改为调用Bar的实现。注意,Baz不必使用new关键字。如果省略关键字,则会收到编译器警告,但是C#中的隐含行为是隐藏父方法。baz2Bai,它会覆盖Baznew实现,因此其行为类似于foo2的;称为Bai的实际对象类型的实现。bai2是一个Bat,再次隐藏其父项Bai的方法实现,并且其行为与bar2即使Bai的实现未密封一样,因此从理论上讲Bat可以重写而不是隐藏该方法。最后,bat2是一个Bak,没有任何一种重要的实现,仅使用其父代的实现。

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


4

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

这是一个例子:

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

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

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

3

您需要制作它virtual,然后在中重写该功能Teacher。在继承并使用基指针引用派生类时,需要使用重写它virtualnew用于base在派生的类引用而非base类引用上隐藏类方法。


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

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

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


2

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

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


2

新的关键字告诉在当前类中的方法将只有当你有存储类型教师的变量班主任的实例工作。或者您可以使用强制转换触发它:((Teacher)Person).ShowInfo()


1

这里的变量“老师”的typeof(Person)类型是,这种类型对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
{

}

1

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

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

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


0

编译器执行此操作是因为它不知道它是一个Teacher。它所知道的只是它是一个Person或从中派生出来的。因此,它所能做的就是调用Person.ShowInfo()方法。


0

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

您应该在可能被覆盖的类中使用virtualoverride。使用virtual了可以通过子类和使用可重写的方法override对于应覆盖这些方法virtual的方法。


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

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

}

这是输出:

人员:我是人员== 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,也获得了派生的Teacher实现,并且将其显式转换为IPerson。再次这是因为Teacher类没有显式实现IPerson.ShowInfo()方法。

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

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

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

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


我认为正确设置格式代码带来了一些挑战。现在没有时间修饰它……
钢铁般的2015年

0

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();
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.