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

144 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我是人

但是,你可以看到它是一个实例Teacher,而不是Person.代码为什么这样做?

Car*_*ten 365

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!");
    }
}
Run Code Online (Sandbox Code Playgroud)

如果被调用,则行为会ShowInfo因实现而异:

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

person = new Student();
person.ShowInfo();    // Shows 'I am a student!'
Run Code Online (Sandbox Code Playgroud)

这两个,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!");
    }
}
Run Code Online (Sandbox Code Playgroud)

关键的区别在于,基础成员Person.ShowInfo不再指向任何地方.这也是为什么你可以创建实例的原因Person(因此它不需要再标记为abstract):

一个虚拟成员的例证在基类里面的

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

public class Student : Person
{
}
Run Code Online (Sandbox Code Playgroud)

代码将被调用如下:

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

person = new Student();
person.ShowInfo();    // Shows 'I am a person!'
Run Code Online (Sandbox Code Playgroud)

图像Student看起来像这样:

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

3.神奇的"新"关键字又称"阴影"

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

使用new-keyword的

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

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

teacher.ShowInfo();    // Prints 'I am a teacher!'
person.ShowInfo();     // Prints 'I am a person!'
Run Code Online (Sandbox Code Playgroud)

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

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

  • [Paint.NET](http://www.getpaint.net/)和一撮复制粘贴; D (35认同)
  • 谢谢你的回答 (9认同)
  • **tl; dr**你用`new`打破了函数的继承并使新函数与超类的函数分开 (8认同)
  • 你用什么来生成这些图表? (6认同)
  • @Taymon:其实不是......我只想说清楚现在的电话是针对"人",而不是"学生";) (3认同)
  • 优秀而且非常彻底的答案. (2认同)
  • 是否需要演员表?(Java程序员在这里.) (2认同)

小智 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

在您的问题中的代码中,您使用的new阴影而不是覆盖.阴影只会影响编译时语义,而不会影响运行时语义,因此会影响非预期的输出.

  • 谁能说OP知道那些意思. (4认同)

Adi*_*dil 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

虚拟方法

调用虚方法时,将检查对象的运行时类型是否有覆盖成员.如果没有派生类重写成员,则调用派生程度最大的类中的重写成员,该成员可能是原始成员.默认情况下,方法是非虚拟的.您无法覆盖非虚方法.您不能将virtual修饰符与static,abstract,private或override修饰符MSDN一起使用.

使用New进行阴影处理

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

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

  • 如果派生类中方法前面带有new关键字,则该方法被定义为独立于基类中的方法,这篇MSDN文章很好地解释了它.

早期绑定VS后期绑定

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

对于普通函数,编译器可以在内存中计算出它的数字位置.然后在调用函数时,它可以生成一个指令来调用该地址的函数.

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


Kei*_*thS 7

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

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

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

因为扩展或覆盖您无法控制的非虚方法的功能通常很有用,所以C#还包括new上下文关键字.所述new关键字"隐藏"父方法,而不是覆盖它.任何可继承的方法都可以隐藏,无论它是否为虚拟; 这允许您(开发人员)利用您希望从父级继承的成员,而不必解决您不使用的成员,同时仍允许您向代码的使用者提供相同的"接口".

隐藏的工作方式类似于从使用您的对象的人的角度覆盖,该对象等于或低于定义隐藏方法的继承级别.从问题的示例中,创建教师并将该引用存储在教师类型的变量中的编码器将看到来自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();
Run Code Online (Sandbox Code Playgroud)

输出:

Foo
Bar
Baz
Bai
Bat
---
Bar
Bar
Bai
Bai
Bat
---
Bar
Bar
Bai
Bai
Bat
Run Code Online (Sandbox Code Playgroud)

第一组五是完全可以预料的; 因为每个级别都有一个实现,并且被引用为与实例化的类型相同的对象,所以运行时将每次调用解析为由变量类型引用的继承级别.

第二组五是将每个实例分配给直接父类型的变量的结果.现在,行为的一些差异摆脱了; foo2,实际上是一个Bar强制转换Foo,仍然会找到实际对象类型Bar的派生方法.bar2是一个Baz,但与之不同foo2,因为Baz没有明确地覆盖Bar的实现(它不能;禁止sealed它),运行时看不到"自上而下",所以调用Bar的实现.请注意,Baz不必使用new关键字; 如果省略关键字,您将收到编译器警告,但C#中的隐含行为是隐藏父方法.baz2Bai,可替换Baznew实施方式中,因此它的行为类似于foo2的; 实际对象类型在Bai中的实现被称为.bai2是一个Bat,它再次隐藏了它的父Bai方法的实现,并且它的行为相同,bar2即使Bai的实现没有被密封,所以理论上Bat可以覆盖而不是隐藏方法.最后,bat2是一个Bak没有任何类型的重写实现,并且只使用其父类的实现.

第三组五个说明了完全自上而下的解决方案行为.实际上,所有内容都是引用链中派生程度最高的类的实例Bak,但是在每个级别的变量类型中的解析都是通过从继承链的该级别开始并向下钻取到方法的最派生显式覆盖来执行的,这些是那些Bar,BaiBat.隐藏的方法因此"打破"了重写的继承链; 您必须使用隐藏方法的继承级别或更低级别的对象,以便使用隐藏方法.否则,隐藏的方法被"揭开"并改为使用.