147

Consider this code:

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

When I run this code, the following is outputted:

I am Person

However, you can see that it is an instance of Teacher, not of Person. Why does the code do that?

4

16 回答 16

370

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 都是s Person,但是当他们被要求提示有关自己的信息时,它们的行为不同。但是,要求他们提示信息的方式是一样的:使用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看起来像这样:

使用 virtual-keyword 的方法的默认实现说明

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

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

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

于 2013-07-18T09:14:39.220 回答
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,它进行阴影而不是覆盖。阴影仅影响编译时语义而不是运行时语义,因此会产生意外输出。

于 2013-07-18T07:48:39.053 回答
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");
    }
}

虚拟方法

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

使用新的阴影

您正在使用 new 关键字而不是 override,这就是 new 所做的

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

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

早期绑定 VS 后期绑定

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

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

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

于 2013-07-18T07:47:56.653 回答
7

我想以Achratt 的回答为基础。为了完整起见,不同之处在于 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# 还允许隐式方法隐藏;简单地定义一个与父类方法具有相同签名的方法,不带overrideor new,将隐藏它(尽管它会产生编译器警告或来自某些重构助手如 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作为 a 的转换Foo,仍然会找到实际对象类型 Bar 的更多派生方法。bar2是 a Baz,但与 with 不同foo2,因为 Baz 没有显式覆盖 Bar 的实现(它不能;Bar sealedit),运行时在“自上而下”查看时看不到它,因此改为调用 Bar 的实现。请注意,Baz 不必使用new关键字;如果省略关键字,则会收到编译器警告,但 C# 中的隐含行为是隐藏父方法。baz2是 a Bai,它覆盖Baz'snew实现,所以它的行为类似于foo2's; 调用 Bai 中实际对象类型的实现。bai2是 a Bat,它再次隐藏了其 parentBai的方法实现,它的行为与bar2即使 Bai 的实现未密封,所以理论上 Bat 可以覆盖而不是隐藏该方法。最后,bat2是 a Bak,它没有任何一种重写实现,只是使用其父类的实现。

第三组五个说明了完整的自上而下的分辨率行为。一切实际上都在引用链中派生程度最高的类的实例Bak,但是变量类型的每一级的解析都是通过从继承链的该级别开始并深入到方法的派生程度最高的显式覆盖来执行的,它们是BarBaiBat中的那些 方法隐藏因此“破坏”了覆盖的继承链;您必须在隐藏方法的继承级别或以下级别使用对象才能使用隐藏方法。否则,隐藏的方法被“发现”并被使用。

于 2013-07-18T18:33:57.273 回答
4

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

这是那里的一个例子:

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

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

BaseClass A = (BaseClass)B;
A.DoWork();  // Calls the old method.
于 2013-07-18T07:49:59.330 回答
3

您需要制作它virtual,然后在Teacher. 当您继承并使用基指针来引用派生类时,您需要使用virtual. new用于将类方法隐藏在base派生类引用而不是base类引用上。

于 2013-07-18T07:49:54.563 回答
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 编译器(智能感知)会将 a.foo() 显示为 A.foo()。

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

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

于 2016-03-23T08:10:23.467 回答
2

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

在 C# 中,您必须在基类中将方法标记为虚拟,然后您将得到您想要的。

于 2013-07-18T07:50:13.743 回答
2

new关键字告诉当前类中的方法仅在您将类 Teacher 类的实例存储在类型为 Teacher 的变量中时才有效。或者您可以使用强制转换触发它: ((Teacher)Person).ShowInfo()

于 2013-07-24T18:44:26.237 回答
1

这里的变量“teacher”的类型是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
{

}
于 2013-07-18T08:05:42.323 回答
1

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

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

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

于 2015-07-07T21:52:15.380 回答
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");
    }
}
于 2013-08-04T20:37:44.227 回答
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 关键字与 virtual 关键字结合使用。基类方法必须是虚拟的。或抽象,在这种情况下,类也必须是抽象的。

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

因为teacher 是Typeof(Teacher) 并且不在Person 继承级别,所以teacher 获得了ShowInfo 的派生Teacher 实现。

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 的实例。

于 2015-04-02T00:41:21.593 回答
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();
    }
}
于 2015-05-01T21:35:47.910 回答
0

只想简单回答一下——

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

于 2013-07-18T21:46:08.897 回答
0
    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 关键字,该方法被定义为独立于基类

当使用 override 时,内存被派生类成员覆盖,在这种情况下,通过AA.Print()slot override by BB.Print(); BB.Print()被 覆盖DD.Print()。当我们打电话时AA aa = new CC();编译器将为它创建新的内存槽,CC.Print()但是当它转换为 AA 时,然后根据 Vtable Map,调用 AA 可覆盖对象 DD。

参考 - c# - 覆盖和隐藏之间的确切区别 - 堆栈内存溢出 .NET Framework 内部:CLR 如何创建运行时对象 | 微软文档

于 2022-01-05T18:34:53.543 回答