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

考虑下面的代码:

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的一个实例?

70326 次浏览

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

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 关键字,则该方法被定义为与基类中的方法无关,这个 MSDN 文章很好地解释了它。

早期装订与晚期装订

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

对于普通函数,编译器可以计算出数值位置 然后,当函数被调用时,它就可以生成 在这个地址调用函数的指令。

对于具有任何虚方法的对象,编译器将生成 这实际上是一个数组,其中包含 虚方法。每个具有虚方法的对象将 包含由编译器生成的隐藏成员,即地址 当一个虚函数被调用时,编译器将 计算出什么位置是适当的方法在 然后它将生成代码来查看对象 在这个位置调用虚方法 参考文献

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,它执行 跟踪而不是重写。跟踪仅影响编译时语义,而不影响运行时语义,因此会产生意外的输出。

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

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

这里有一个例子:

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

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


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

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

在 C # 中,你必须在基类中将一个方法标记为虚方法,然后你就可以得到你想要的了。

这里的变量‘ acher’的类型是 typeof(Person),这个类型不知道任何有关教师类的信息,也不会尝试在派生类型中查找任何方法。要调用教师类的方法,您应该强制转换变量: (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
{


}

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实例时,它指向 Students 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,它们 可以(而不是 virtual0)为 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!'

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

我希望这能让你明白一些事情!

我想建立在 阿克拉特的回答的基础上。为了完整起见,区别在于 OP 期望派生类方法中的 new关键字重写基类方法。它实际上做的是 藏起来基类方法。

在 C # 中,正如另一个答案所提到的,传统的方法重写必须是显式的; 基类方法必须标记为 virtual,派生类必须明确地标记为 override基类方法。如果这样做了,那么对象是作为基类的实例还是作为派生类的实例并不重要; 找到并调用派生方法。这与 C + + 中的方式类似; 在编译时,标记为“虚拟”或“覆盖”的方法通过确定被引用对象的实际类型,并沿着从变量类型到实际对象类型的树向下遍历对象层次结构,以找到由变量类型定义的方法的最派生实现,从而在“后期”(运行时)得到解析。

这与 Java 不同,Java 允许“隐式重写”; 对于实例方法(非静态) ,简单地定义一个具有相同签名的方法(名称和参数的数量/类型)将导致子类重写超类。

因为扩展或覆盖不受控制的非虚方法的功能通常很有用,所以 C # 还包含 new上下文关键字。new关键字“隐藏”父方法而不是重写它。任何可继承的方法都可以被隐藏,不管它是否是虚拟的; 这允许你,开发者,利用你想要从父代继承的成员,而不必绕过那些你不需要的成员,同时仍然允许你向代码的使用者呈现相同的“接口”。

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

此外,不仅 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 的实现(它不能; Bar sealed它) ,所以当查看“自顶向下”时,运行时不会看到它,因此调用 Bar 的实现。请注意,Baz 不必使用 new关键字; 如果省略该关键字,将得到编译器的警告,但是 C # 中隐含的行为是隐藏父方法。baz2是一个 Bai,它覆盖了 Baznew实现,因此它的行为类似于 foo2; 实际的对象类型的白实现被调用。Bar3是一个 Bar4,它再次隐藏了它的父 Bai的方法实现,并且它的行为和 bar2一样,即使白的实现没有被密封,所以理论上蝙蝠可以覆盖而不是隐藏方法。最后,Bar7是一个 Bar8,它没有这两种类型的重写实现,只是使用其父类的重写实现。

第三组五个例子说明了完全自顶向下的分辨率行为。实际上,所有东西都引用了链中最派生类 Bak的一个实例,但是每个级别的变量类型的解析都是通过从继承链的那个级别开始,然后深入到方法的最派生的 露骨覆盖,即 BarBaiBat中的 露骨覆盖来完成的。方法隐藏因此“破坏”了重写的继承链; 为了使用隐藏方法,必须在隐藏方法的继承级别或更低级别处理对象。否则,藏起来了方法将被“发现”并被替代使用。

我只想简单回答一下

应该在可以重写的类中使用 virtualoverride。对于可被子类重写的方法使用 virtual,对于应重写此类 virtual方法的方法使用 override

新的关键字告诉我们,当前类中的方法只有在将类 Teacher 的一个实例存储在一个类型为 Teacher 的变量中时才能正常工作。或者你可以用铸件触发它: ((教师)人)。ShowInfo ()

除了一些变化之外,我编写的代码和你上面提到的 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 的出色演示和其他人的高质量答案,并为了超级完整性,让我们继续前进,抛出显式的接口实现来演示如何工作。考虑以下几点:

命名空间 {

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

}

输出如下:

我是老师

老师: 我是老师 = = LinqConsole 应用程序

人物1: 我是老师 = = LinqConsole 应用程序。老师

人物2: 我是老师 = = LinqConsole 应用程序。老师

老师1: 我是老师 = = LinqConsole 应用程序。老师

我是 Person = = LinqConsolApp.Person

我是 interface Person = = LinqConsolApp.Person

有两件事要注意:
老师。ShowInfo ()方法省略 new 关键字。省略 new 时,方法行为与显式定义 new 关键字时相同。

只能将覆盖关键字与虚拟关键字结合使用。基类方法必须是虚方法。或者抽象,在这种情况下类也必须是抽象的。

Person 获取 ShowInfo 的基本实现,因为教师类不能覆盖基本实现(没有虚拟声明) ,而 person 是。GetType (教师) ,因此它隐藏了教师类的实现。

教师得到 ShowInfo 的派生的教师实现是因为教师,因为它是 Typeof (教师) ,而且不在 Person 继承级别上。

Person1获取派生的教师实现,因为它是。 GetType (教师)和隐含的新关键字隐藏了基本实现。

Person2还获取派生的教师实现,即使它确实实现了 IPerson,并且获取了对 IPerson 的显式强制转换。这也是因为教师类没有显式实现 IPerson。ShowInfo ()方法。

还获取派生的教师实现,因为它是.GetType (教师)。

只有 Person 3获取 ShowInfo 的 IPerson 实现,因为只有 Person 类显式实现该方法,而 Person 3是 IPerson 类型的实例。

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

注意,连 person 4都没有得到 IPerson。ShowInfo 实现。这是因为即使人4是。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();
}
}

也许为时已晚... ... 但问题很简单,答案也应该具有同样的复杂性。

在您的代码变量中,person 不知道任何关于 Teacher.ShowInfo ()的信息。 无法从基类引用调用 last 方法,因为它不是虚方法。

继承有一种有用的方法——试着想象一下您想用代码层次结构说些什么。还要试着想象一个或另一个工具是如何描述它自己的。例如,如果你将虚函数添加到一个基类中,你假设: 1。它可以有默认的实现; 2。它可以在派生类中重新实现。如果你添加抽象函数,这意味着只有一件事-子类必须创建一个实现。但是,如果你有普通的功能-你不希望任何人改变它的实现。

我想添加一对夫妇更多的例子,以扩大这方面的信息。希望这也有所帮助:

下面是一个代码示例,它清除了将派生类型分配给基类型时发生的情况。哪些方法可用,以及此上下文中重写方法和隐藏方法之间的区别。

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

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

这个代码示例应该有助于描述这些注意事项!

    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 关键字时,将为 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 如何创建运行时对象 | MicrosoftDocs