一道面试题
标题取的有点不恰当,其实这不是一道面试题,而是在不同的面试中出现的一道类似的题~
面试题目的大概代码如下:
using System; namespace view0 { class Program { static void Main(string[] args) { B ab = new B(); sayHello(ab); Console.Read(); } private static void sayHello(A b) { b.haha(); b.hehe(); (b as B).hehe(); } } internal class A { public void hehe() { Console.WriteLine("Nonvirtual HeHe from A"); } public virtual void haha() { Console.WriteLine("Virtual HaHa from A"); } protected virtual void xixi() { Console.WriteLine("XiXi"); } public static void heihei() { Console.WriteLine("Static HeiHei from A"); } } internal sealed class B : A { public new void hehe() { Console.WriteLine("Nonvirtual HeHe from B"); } public override void haha() { Console.WriteLine("Virtual HaHa from B"); xixi(); } } }
输出如下:
按照我对《clr via C#》一书中对方法调用的理解,实际的过程如下:
(1)程序先会创建main()方法中需要的类型(通过加载类型所在的程序集来保证类型能够被创建),这里主要会创建A和B两个类型。其中除了每个类型需要的内存外还会额外创建类型对象指针和同步索引块。(类型对象指针的作用下面会提及,同步索引块应该是因为每次垃圾回收后需要对堆上的内存重新排列,所以每个堆上分配的内存块需要一个索引)
(2)然后会在sayHello()调用类型为A的对象B的参数b的虚方法(好绕),简单说就是b的类型对象指针指向的是B,而变量b的类型却为A。而C#中虚方法的调用是通过对象的类型而不是变量类型来决定的。所以会调用类B的那个重写方法haha()。书中的解释如下
调用一个虚实例方法时,JIT编译器要在方法中生成一些额外的代码;方法每次调用时,都会执行这些代码。这些代码首先检查发出调用的变量,然后跟随地址来到发出调用的对象。然后,代码检查对象内部的“类型对象指针”成员,这个成员指向对象的实际类型。然后,代码在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT编译。
(3)而下一步会调用b的一个非虚的实例方法hehe(),这时候JIT会找的b的类型(A)的类型对象指针,然后会在A中寻找hehe()这个方法,找不到会回溯类层次结构。书中的解释如下
调用一个非虚实例方法时,IT编译器会找到与“发出调用的那个变量的类型”对应的类型对象。如果类型没有定义正在调用的那个方法,JIT编译器会回溯类层次结构(一直回溯Object),并在沿途的每个类型中查找该方法。之所以能这样回溯,是因为每个类型对象都有一个字段引用了它的基类型,
这里我们可以知道上面的输出是如何来的,而调用一个虚的实例方法因为要知道对象的类型,所以对象不能为空,因此要对对象经行检查。所以调用一个虚的实例方法会比调用一个非虚实例方法的性能差一点。不过由于C#中调用非虚的实例方法用的IL命令也是要检查调用对象是否非空的,所以实际的效率应该差不多。