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

考虑这个代码:

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有区别。

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

方法实现的插图

现在有不同的方法,可以定义一个方法。 与inheritance一起使用时,每个行为都会有所不同。 标准的方式总是像上面的图像说明的那样工作。 如果你想改变这种行为,你可以附加不同的关键字到你的方法。

1.抽象类

第一个是abstractabstract方法简单地指向无处:

抽象类的例证

如果你的类包含抽象成员,它也需要被标记为abstract ,否则编译器不会编译你的应用程序。 您不能创buildabstract类的实例,但是您可以inheritance它们并创build您的inheritance类的实例,并使用基类定义来访问它们。 在你的例子中,这看起来像:

 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 ,但是当他们被要求提示自己的信息时,他们的performance会有所不同。 但是,要求他们提示他们信息的方式是一样的:使用Person类接口。

那么当你从Personinheritance的时候幕后会发生什么呢? 在实现ShowInfo ,指针不再指向任何地方 ,现在指向实际的实现! 当创build一个Student实例时,它指向Student 's ShowInfo

继承方法的插图

2.虚拟方法

第二种方法是使用virtual方法。 行为是一样的,除非你在你的基类中提供了一个可选的默认实现。 具有virtual成员的类可以实例化,但是inheritance类可以提供不同的实现。 以下是你的代码应该看起来像工作:

 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不再指向任何地方 。 这也是为什么你可以创buildPerson实例(因此不需要被标记为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的图像看起来像这样:

一个方法的默认实现的插图,使用virtual-keyword

3.魔术“新”关键词又名“影子”

new的更多的是在这附近。 您可以在通用类中提供方法,它们与基类/接口中的方法具有相同的名称。 都指向自己,自定义实现:

使用new关键字的“解决方法”的插图

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

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

这种行为可能是需要的,但在你的情况下,这是误导。

我希望这能让事情变得更加清晰,让你明白!

C#中的子types多态使用显式虚拟,类似于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"); } } 

虚拟方法

当一个虚拟方法被调用时,该对象的运行时types被检查一个覆盖成员。 如果没有派生类重写成员,则派生类中的重写成员被调用,可能是原始成员。 默认情况下,方法是非虚拟的。 您不能覆盖非虚拟方法。 您不能使用具有静态,抽象,私人或超驰修饰符, MSDN的虚拟修饰符。

使用新的阴影

你正在使用新的关键词,而不是重写,这是新的

  • 如果派生类中的方法没有使用新的或覆盖的关键字,编译器将发出一个警告,并且该方法的行为就像存在新的关键字一样。

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

早绑定VS晚绑定

我们在编译时为正常方法提供了早期的绑定(不是虚拟的),这是编译器绑定调用基类方法的引用types(基类)的方法,而不是基类的引用类即派生类对象 。 这是因为ShowInfo不是一个虚拟的方法。 使用虚拟方法表 (vtable)在运行时执行后期绑定(虚拟/重写方法)。

对于正常的函数,编译器可以在内存中计算出它的数字位置。 然后当它被调用的时候,它可以产生一个指令来调用这个地址的函数。

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

我想build立Achratt的答案 。 为了完整,区别在于OP期望派生类的方法中的new关键字覆盖基类方法。 它实际上做的是隐藏基类的方法。

在C#中,作为另一个提到的答案,传统的方法重写必须是明确的; 基类方法必须被标记为virtual ,并且派生类必须专门override基类方法。 如果这样做,那么对象是否被视为基类或派生类的实例并不重要; 派生的方法被发现和调用。 这是以类似于C ++的方式完成的; 在编译时标记为“虚拟”或“覆盖”的方法通过确定被引用对象的实际types并沿着树从variablestypes向下遍历对象层次结构到实际对象types来“parsing”(在运行时)find由variablestypes定义的方法的最派生的实现。

这不同于Java,它允许“隐式覆盖”; 例如方法(非静态),简单地定义相同签名(名称和数字/参数types)的方法将导致子类重写超类。

因为扩展或覆盖不受控制的非虚方法的function通常很有用,C#还包含new上下文关键字。 new关键字“隐藏”父方法,而不是覆盖它。 任何可inheritance的方法都可以被隐藏,不pipe它是否是虚拟的; 这使开发人员能够利用想要从父代inheritance的成员,而无需解决那些不需要的成员,同时仍然允许向代码的使用者提供相同的“接口”。

隐藏的工作方式类似于从隐藏方法被定义的inheritance层或低于隐藏层的人的angular度来覆盖。 从问题的例子来看,一个编码器创build一个教师,并将其存储在一个教师types的variables中,将看到来自教师的ShowInfo()实现的行为,这个行为隐藏了Person中的一个。 但是,有人在你的对象中使用你的对象,你将会看到Person的实现ShowInfo()的行为。 因为教师的方法没有覆盖其父(这也需要Person.ShowInfo()是虚拟的),在Person的抽象级别工作的代码将不会find教师的实现,不会使用它。

另外, new关键字不仅明确地做到这一点,C#允许隐式的方法隐藏; 简单地定义一个具有与父类方法相同的签名的方法,而不用overridenew ,将会隐藏它(尽pipe它会产生编译器警告或来自ReSharper或CodeRush等重构助手的抱怨)。 这是C#的devise者在C ++的显式覆盖与Java隐式代码之间提出的妥协,虽然它很优雅,但如果你是来自任何一种老版本的语言的背景,它并不总是产生你期望的行为。

这里是新的东西:当你在一个长的inheritance链中组合这两个关键字时,这会变得很复杂。 考虑以下几点:

 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 

第一组五个都是可以预料的; 因为每个级别都有一个实现,并且被引用为与实例化types相同的对象,所以运行时将每次调用都parsing为由variablestypes引用的inheritance级别。

第二组五是将每个实例分配给直接父types的variables的结果。 现在,一些行为上的差异就摆脱了; foo2 ,实际上是一个Bar作为Foo ,仍然会find实际对象typesBar的更多派生方法。 bar2是一个Baz ,但与foo2不同,因为Baz并没有明确地重写Bar的实现(Bar不能,Bar sealed它),当从上到下看时,运行时没有看到它,所以Bar的实现被调用。 请注意,Baz不必使用new关键字; 如果您省略关键字,则会得到编译器警告,但C#中隐含的行为是隐藏父级方法。 baz2是一个压倒Baz new实现的白,所以它的行为与foo2相似, 在Bai中实际的对象types的实现被调用。 bai2是一个Bat ,它再次隐藏了其父方法的实现,尽pipeBai的实现不封闭,它的performance与bar2相同,理论上Bat可以重写而不是隐藏方法。 最后, bat2是一个Bak ,它没有任何一种压倒一切的实现,只是使用它的父类。

第三组五个图表示完整的自顶向下parsing行为。 所有事实上都引用了链中最派生类的一个实例Bak ,但是在variablestypes的每个级别上的parsing都是通过从inheritance链的这个级别开始并深入到最大派生的方法的显式重写来执行的,是那些在BarBaiBat 。 隐藏的方法“打破”了压倒一切的inheritance链; 您必须使用处于隐藏方法的inheritance级别或低于该级别的对象,才能使用隐藏方法。 否则, 隐藏的方法是“揭露”,而不是使用。

请阅读C#中的多态性 : 多态(C#编程指南)

这是一个例子:

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

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

您需要将其设置为virtual ,然后在“ Teacher覆盖该function。 由于您inheritance和使用基指针来引用派生类,您需要使用virtual来覆盖它。 new是用于隐藏派生类引用上的base类方法,而不是base类引用。

我想添加一些更多的例子来扩展这个信息。 希望这也有帮助:

下面是一个代码示例,它清楚了将派生types分配给基types时发生的情况。 在这种情况下,哪些方法是可用的,以及重写方法和隐藏方法的区别。

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

另一个小exception是,对于下面这行代码:

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

VS编译器(智能感知)将显示a.foo()作为A.foo()。

因此,很明显,当更多的派生types被分配给基types时,“基types”variables作为基types,直到在派生types中被覆盖的方法被引用为止。 这可能与父types和子types之间具有相同名称(但未覆盖)的隐藏方法或方法有点反直觉。

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

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

在C#中,你必须在基类中标记一个虚拟的方法,那么你将得到你想要的。

new关键字告诉当前类中的方法只有在Teacher类中存储了Teacher类的variables时才能使用。 或者你可以使用castings来触发它:((Teacher)Person).ShowInfo()

这里的variables'teacher'的types是typeof(Person) ,这个types对Teacher类没有任何认识,也不会去寻找派生类中的任何方法。 要调用教师课堂的方法,您应该投入您的variables:( (person as Teacher).ShowInfo()

要调用基于值types的特定方法,您应该在您的基类中使用关键字“虚拟”,并重写派生类中的虚拟方法。 这种方法允许在有或没有重写虚拟方法的情况下实现派生类。 基类的方法将被调用,而没有被覆盖的虚拟types。

 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 。 所有它知道的是,这是一个Person或从中派生出来的东西。 所以它只能调用Person.ShowInfo()方法。

只是想简要回答一下 –

您应该使用virtualoverride可能被覆盖的类。 对于可以被子类override的方法使用virtual方法,并使用override方法来覆盖这些virtual方法。

我写了相同的代码,因为你在java中提到的除了一些改变,它作为例外运行良好。 基类的方法被覆盖,所以显示的输出是“我是老师”。

原因:我们正在创build一个基类的引用(它能够引用派生类的实例),它实际上包含派生类的引用。 而且我们知道,实例总是先查看它的方法,如果它发现它在那里执行它,如果它没有find它的定义,那么它在层次结构中上升。

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

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

在你的代码variables中,人们对Teacher.ShowInfo()一无所知。 没有办法从基类引用调用最后的方法,因为它不是虚拟的。

有一个有用的方法来inheritance – 尝试想象你想要说你的代码层次结构。 也试图想象一个或另一个工具对自己说什么。 例如,如果您将虚拟函数添加到基类中,则您可以这样做:1.可以具有默认实现; 2.它可能会在派生类中重新实现。 如果添加抽象函数,则意味着只有一件事 – 子类必须创build一个实现。 但是如果你有简单的function – 你不希望任何人改变它的实现。

基于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 == 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时,方法行为与新的关键字明确定义相同。

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

人获得ShowInfo的基本实现,因为Teacher类不能覆盖基本实现(没有虚拟声明),而person是.GetType(Teacher),所以隐藏了Teacher类的实现。

因为教师是Typeof(Teacher),并且它不在Personinheritance级别上,所以教师获得ShowInfo的派生教师实现。

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

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

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

只有person3获得ShowInfo的IPerson实现,因为只有Person类显式实现该方法,而person3是IPersontypes的一个实例。

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

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

LinQPad样品盲目启动,减less重复的代码,我认为是你正在尝试做的。

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