【问题标题】:Virtual Functions C#虚函数 C#
【发布时间】:2011-03-15 04:14:16
【问题描述】:

我了解什么是虚函数。但我不明白它们是如何在内部工作的?

class Animal
{
    virtual string Eat()
    {
        return @"Eat undefined";
    }
}

class Human : Animal
{
    override string Eat()
    {
         return @"Eat like a Human";
    }
}


class Dog : Animal
{
    new string Eat()
    {
         return @"Eat like a Dog";
    }
}

static void Main()
{
    Animal _animal = new Human();
    Console.WriteLine(_animal.Eat());
    _animal = new Dog();
    Console.WriteLine(_animal.Eat());
}

上述输出给出:

Eat like a Human
Eat undefined

在上面的代码中,_animal 是 Animal 类型,它引用了 Human 对象或 Dog 对象。 这是什么意思?我知道在内存中 _animal 包含一个指向 Human 或 Dog 对象的地址。它如何决定调用哪个函数。在第一种情况下,我覆盖并因此调用子级的实现,但在第二种情况下,我使用 new 并因此调用父级的实现。你能解释一下幕后发生了什么吗?

提前致谢 尼克

【问题讨论】:

标签: c# methods virtual


【解决方案1】:

它是这样工作的。想象一下编译器把你的类改写成这样:

class VTable
{
    public VTable(Func<Animal, string> eat)
    {
        this.AnimalEat = eat;
    }
    public readonly Func<Animal, string> AnimalEat;
}

class Animal
{
    private static AnimalVTable = new VTable(Animal.AnimalEat);
    private static string AnimalEat(Animal _this)
    { 
        return "undefined"; 
    }
    public VTable VTable;
    public static Animal CreateAnimal() 
    { 
        return new Animal() 
            { VTable = AnimalVTable }; 
    }
}

class Human : Animal
{
    private static HumanVTable = new VTable(Human.HumanEat); 
    private static string HumanEat(Animal _this)
    {
        return "human"; 
    }
    public static Human CreateHuman()
    {
        return new Human() 
            { VTable = HumanVTable };
    }
}

class Dog : Animal
{
    public static string DogEat(Dog _this) { return "dog"; }
    public static Dog CreateDog()
    {
        return new Dog() 
            { VTable = AnimalVTable } ;
    }
}

现在考虑这些调用:

Animal animal;
Dog dog;
animal = new Human();
animal.Eat();
animal = new Animal();
animal.Eat();
dog = new Dog();
dog.Eat();
animal = dog;
animal.Eat();

编译器的原因如下:如果接收者的类型是Animal,那么Eat的调用必须是animal.VTable.AnimalEat。如果接收者的类型是 Dog,那么调用必须是 DogEat。所以编译器把这些写成:

Animal animal;
Dog dog;
animal = Human.CreateHuman(); // sets the VTable field to HumanVTable
animal.VTable.AnimalEat(animal); // calls HumanVTable.AnimalEat
animal = Animal.CreateAnimal(); // sets the VTable field to AnimalVTable
animal.VTable.AnimalEat(animal); // calls AnimalVTable.AnimalEat
dog = Dog.CreateDog(); // sets the VTable field to AnimalVTable
Dog.DogEat(dog); // calls DogEat, obviously
animal = dog;
animal.VTable.AnimalEat(animal); // calls AnimalVTable.AnimalEat

正是它的工作原理。编译器会在后台为你生成 vtable,并在编译时根据重载解析的规则决定是否通过 vtable 调用

vtables 是在创建对象时由内存分配器设置的。 (我的草图在这方面是一个谎言,因为 vtable 是在调用 ctor 之前 设置的,而不是之后。)

虚方法的“this”实际上是作为不可见的形参偷偷传递给方法的。

有意义吗?

【讨论】:

  • @Eric:嘿,Eric,我注意到您将您的信息从高级开发人员更新为首席开发人员。我有 3 天没在 SO 上,所以我假设你周末更新了:O 只是想祝贺你的新职位/晋升,如果我可以这么说,你应该得到它。这是否也意味着 C# 的任何东西都会通过你?我希望如此,因为您是 IMO 那里最好的开发人员之一。
  • @Joan:谢谢你的好话。当然,我离 C# 的最终结论还很远。我是这个团队中资历较浅的成员之一;我只有十五年的时间来设计和实现编程语言。我与 Anders Hejlsberg、Neal Gafter 和 Peter Golde 一起工作,仅举几例。那些家伙比我年长得多。
  • @Eric: 还以为自己是大三就好笑了,我不知道你下面会是什么人 :O 我工作的地方,虽然不是软件公司,但是校长很高高的。但我猜在微软这样的软件巨头中,两者之间的等级要高得多。不管怎样,能够与​​你们交谈都是非常宝贵的经验。期待看到你在等级和幸福中上升。
  • 对于那些更熟悉 C++ 的人,可能值得指出的是 Animal.VTable,它是一个 VReference。 C++ 编译器通常使用 VPointer,但 VReference 也可以正常工作。无论哪种情况,它都引用或指向对应于对象实际类型的静态 VTable,而不是您调用函数所通过的变量或表达式的类型。
  • @Eric:是的,这绝对有意义。它帮助我现在获得了更好的照片。非常感谢。
【解决方案2】:
我知道内存中的_animal 包含一个指向Human 或Dog 对象的地址。它如何决定调用哪个函数。

和数据一样,代码也有地址。

因此,解决此问题的典型方法是让HumanDog 对象包含其方法的代码地址。这有时被称为使用vtable。在像 C 或 C++ 这样的语言中,这个概念也直接暴露为所谓的function pointer

现在,您提到了 C#,它有一个相当高级的类型系统,其中对象的类型在运行时也是可辨别的……因此,实现细节可能在某些方面与传统方法不同。但是,至于您的问题,函数指针/v-table 概念是一种方法,如果 .NET 偏离这一点太多,我会感到惊讶。

【讨论】:

  • 谢谢。你能帮我分配内存吗?你说代码也有一个地址。在这种情况下如何分配内存?在 Dog 类中它只是隐藏基类方法,而在 Human 类中它覆盖。
  • @nick - 当您的 EXE 或 DLL 被加载时,操作系统会为其代码管理内存。 (在 C# 的情况下,还涉及 JIT,因此 .NET 运行时也会管理所涉及的内存。)
  • 解决最后一句话中的要点:您可能会对接口的工作方式感到有些惊讶。抖动通常会为类层次结构上的虚拟方法调用生成“经典”vtable。但是,为接口方法上的虚拟调用生成的代码要复杂一些。它与 C++ 编译器生成的典型间接 vtable 不同。
【解决方案3】:

在 C# 中,派生类必须为从基类继承的任何重写方法提供 override 修饰符。

Animal _animal = new Human();

这不仅仅是Human 对象的构造。它们是两个子对象。一个是Animal 子对象,另一个是Human 子对象。

Console.WriteLine(_animal.Eat());

当调用_animal.Eat(); 时,运行时检查派生类中是否覆盖了基类方法(即Eat())。因为,它被覆盖,相应的派生类方法被调用。因此输出 -

Eat like a Human

但是,如果是-

_animal = new Dog();
Console.WriteLine(_animal.Eat());

Dog中,派生类Dog中没有Eat()重写方法。因此,基类方法本身被调用。这种检查方法也被完成,因为在基类中,Eat() 被提及为虚拟并且调用机制在运行时决定。综上所述,虚拟调用机制是一种运行时机制。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-08-27
    • 1970-01-01
    • 2010-12-13
    • 2015-06-17
    • 2013-07-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多