2011-03-15 147 views
3

我明白虚拟函数是什么。但我没有得到的是他们如何在内部工作?虚拟函数C#

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包含一个地址,它将指向人类或狗对象。它如何决定调用哪个函数。在第一种情况下,我重写,因此调用了孩子的实现,但在第二种情况下,我使用新的,因此调用了父代的实现。你能解释一下发生了什么?

在此先感谢 尼克

+1

你知道Eric Lippert正在撰写关于这个主题的博客系列吗?请参阅http://blogs.msdn.com/b/ericlippert/archive/2011/03/17/implementing-the-virtual-method-pattern-in-c-part-one.aspx – Learner 2011-03-23 08:56:15

+0

谢谢学习者。我正在关注它:) – Nishant 2011-04-05 03:03:14

回答

17

它是这样工作的。试想一下,编译器改写了你的类变成这样:

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.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 

也就是说正是它是如何工作的。编译器在后台为您生成vtables,并且根据重载分辨率的规则在编译时决定是否通过vtable调用。

当创建对象时,vtables由内存分配器设置。 (我的素描是这方面的一个谎言,因为虚函数表设置前的构造函数被调用,而不是之后。)

的“本”的虚方法实际上是秘密为一种无形的形式参数来传递方法。

有意义吗?

+7

@Eric:嗨,Eric,我注意到你已经从高级开发人员更新为主要开发人员。我在3天内没有参加,所以我假设你在周末更新了:O只是想对你的新职位/晋升表示祝贺,如果我可以这样说的话,你应该得到它。这是否意味着任何事情和C#都会通过你?我希望如此,因为你是IMO上最好的开发人员之一。 – 2011-03-15 17:08:44

+0

@Joan:感谢客气的话。当然,我对C#上的最后一句话还远远不够。我是这支球队中较为年轻的一员,我只有15年的设计和实现编程语言。我和Anders Hejlsberg,Neal Gafter和Peter Golde一起工作,仅举几例。那些家伙远比我高。 – 2011-03-15 18:56:05

+3

@Eric:如果你仍然认为自己是初中生,这很有趣,我不知道你下面的人会是什么样的人:O我从哪里工作,即使不是软件公司,校长也很高。但我猜想像微软这样的软件巨头,其间的成绩要高得多。无论哪种方式,虽然这将是一个非常宝贵的经验,能够与你们交谈。期待看到你在行列和幸福中上升。 – 2011-03-15 19:33:25

0
我的记忆_animal理解包含将指向人或犬对象的地址。它如何决定调用哪个函数。

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

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

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

+0

谢谢。你能帮我解决内存分配问题吗?你说代码也有一个地址。在这种情况下如何分配内存?在Dog类中,它仅仅隐藏了基类方法,而在Human类中它却覆盖了它。 – Nishant 2011-03-15 04:45:01

+0

@nick - 当您的EXE或DLL被加载时,操作系统会为其代码管理内存。 (在C#的情况下,也包含JIT,因此.NET运行时也会管理涉及的内存。) – asveikau 2011-03-15 05:03:52

+0

为了解决最后一句中的问题:您可能会对接口的工作方式略感意外。抖动通常为类层次上的虚拟方法调用生成“经典”vtables。但是,为接口方法上的虚拟调用生成的代码稍微复杂一些。它与C++编译器生成的典型间接vtable不同。 – 2011-03-15 06:59:57

0

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

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()被称为虚拟并且调用机制被确定为运行时间。综上所述,虚拟调用机制是一种运行时机制。