(原文发表于CSDN我的Blog:http://blog.csdn.net/happyhippy/archive/2006/10/02/1317830.aspx)
昨天在某论坛上看到这个问题,觉得有点意思,就贴过来,顺便贴下我对该问题的思考。
具体问题是这样的:
class GrandFatherClass
{
public virtual void Func() {}
}
Father类重写了这个方法
class FatherClass:GrandFatherClass
{
public override void Func() {}
}
现在Child类想直接调用GrandFather类中的Func要怎么做?
class ChildClass:FatherClass
{
public void OtherFunc()
{
((GrandFatherClass)this).Func();//不能调用GrandFatherClass中的Func().
//GrandFatherClass.Func()如何调用?
}
}
我用ILDasm工具反汇编上面代码编译生成的程序集,得到如下MSIL代码:
{
// 代码大小 11 (0xb)
.maxstack 8
IL_0000: ldstr "GrandFather"
IL_0005: call void [mscorlib]System.Console::WriteLine(string)
IL_000a: ret
} // end of method GrandFatherClass::Func
.method public hidebysig virtual instance void Func() cil managed
{
// 代码大小 11 (0xb)
.maxstack 8
IL_0000: ldstr "Father"
IL_0005: call void [mscorlib]System.Console::WriteLine(string)
IL_000a: ret
} // end of method FatherClass::Func
.method public hidebysig instance void OtherFunc() cil managed
{
// 代码大小 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: callvirt instance void MyProject.GrandFatherClass::Func()
IL_0006: ret
} // end of method ChildClass::OtherFunc
我查了下MSDN,找到了这句话:“调用虚方法时,将为重写成员检查该对象的运行时类型。将调用大部分派生类中的该重写成员,如果没有派生类重写该成员,则它可能是原始成员。”也就是说,即使我们在ChildClass中执行((GrandFatherClass)this).Func()(从IL代码中我们也可以看到是在调用GrandFather的Func()方法),CLR检查到对象的运行时类型为ChildClass,而且ChildClass有重写了自己GrandFatherClass中的Func()(从FatherClass中继承而来),所以该语句仍会调用从FatherClass继承而来的Func()方法。
这里只是从表象上解释了原因,下面这篇《深入探索.NET框架内部了解CLR如何创建运行时对象》则从底层剖析了CLR对象模型,理解了CLR对象模型,我们就可以更加清楚地理解CLR中的方法分派(Dispatch)机制。
用IL和方法表布局来解释如上代码中的行为就是:在方法表中,CLR赋予每个虚方法一个方法槽(Slot),它将包含一个指向方法代码的指针(实际上是通过MethodDesc间接来指向该地址,上面这篇《深入探索……》里面将得比较清楚了,我不再赘述);FatherClass重写了GrandFather的Func()虚方法,它替换被覆盖的虚方法(Func),Func方法槽指向FatherClass实现的Func方法的地址。虚分派总是通过一个固定的槽编号Func()发生,和方法表指针在特定的类(类型)实现层次无关。在方法表布局时,类加载器用覆盖的子类的实现FatherCalss.Func()代替父类的实现GrandFather.Func()。结果,对父对象的方法调用被分派到子对象的实现。
下图是运行时对ChildClass中OtherFunc()反汇编得到的结果(可在调试时通过在命令窗口中输入disasm,可以查看IA-32汇编指令):
从图中我们可以看到,调用一个虚方法要执行三条IA-32汇编指令:
mov ecx,esi ;将目标对象的引用(在这里是this)存储在IA-32 ecx寄存器中
mov eax,dword ptr[ecx] ;这是针对虚方法调用的指令,将对象的类型句柄存储在eax寄存器中
call dword ptr [eax+offset] ;通过对象的类型句柄和方法在方法表中的偏移量来定位目标方法的实际地址
从图中我们也可以看到,不论我们执行((FatherClass)this).Func()还是执行((GrandFatherClass)this).Func(),都是在调用偏移量为38h所指向的方法(ChildClass的Func()只有这一个插槽)。
所以按照上面继承/重写的写法,不能实现调用GrandFatherClass中的Func()。要实现调用GrandFather中的方法,可按如下两种方法:
法一:
{
public virtual void Func() { Console.WriteLine("GrandFather"); }
}
class FatherClass : GrandFatherClass
{
public new virtual void Func() { Console.WriteLine("Father"); }
//加不加关键字new都没有关系,这里加new的作用只是消除编译器警告信息,不会对生成的IL代码产生任何影响。
}
class ChildClass : FatherClass
{
public void OtherFunc()
{
((GrandFatherClass)this).Func();
}
}
//对应的MSIL代码:
.method public hidebysig newslot virtual instance void Func() cil managed
{
// 代码大小 11 (0xb)
.maxstack 8
IL_0000: ldstr "GrandFather"
IL_0005: call void [mscorlib]System.Console::WriteLine(string)
IL_000a: ret
} // end of method GrandFatherClass::Func
.method public hidebysig newslot virtual instance void Func() cil managed
{
// 代码大小 11 (0xb)
.maxstack 8
IL_0000: ldstr "Father"
IL_0005: call void [mscorlib]System.Console::WriteLine(string)
IL_000a: ret
} // end of method FatherClass::Func
.method public hidebysig instance void OtherFunc() cil managed
{
// 代码大小 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: callvirt instance void MyProject.GrandFatherClass::Func()//这里生成的是callvirt调用命令
IL_0006: ret
} // end of method ChildClass::OtherFunc
从IL代码中我们可以看到,FatherClass::Func上有应用newslot标示,CLR赋予被申明为newslot的虚方法一个新的methodoffset,所以这里并没有覆盖GrandFatherClass::Func(),而在FatherClass的方法表中的方法槽表(Method Slot Table)中,也同时存在两个Func()槽,一个是继承而来的,其指向GrandFather的Func()实现,另一个槽执行自身的实现。而ChildClass继承自FatherClass,所以ChildClass中的方法表中,也有两个Func()槽。在下图中,我们也可以看到,这两个方法在方法表中偏移量,一个为38H,另一个为3CH。
法二:
{
public void Func() { Console.WriteLine("GrandFather"); }
}
class FatherClass : GrandFatherClass
{
public new void Func() { Console.WriteLine("Father"); }
}
class ChildClass : FatherClass
{
public void OtherFunc()
{
((GrandFatherClass)this).Func();
}
}
//MSIL:
.method public hidebysig instance void Func() cil managed
{
// 代码大小 11 (0xb)
.maxstack 8
IL_0000: ldstr "GrandFather"
IL_0005: call void [mscorlib]System.Console::WriteLine(string)
IL_000a: ret
} // end of method GrandFatherClass::Func
.method public hidebysig instance void Func() cil managed
{
// 代码大小 11 (0xb)
.maxstack 8
IL_0000: ldstr "Father"
IL_0005: call void [mscorlib]System.Console::WriteLine(string)
IL_000a: ret
} // end of method FatherClass::Func
.method public hidebysig instance void OtherFunc() cil managed
{
// 代码大小 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void MyProject.GrandFatherClass::Func()//这里生成的是call调用命令
IL_0006: ret
} // end of method ChildClass::OtherFunc
仔细观察IL代码,我们会发现这里生成的是call指令(而前面两个调用虚方法时生成的是callvirt指令),IL中的call指令生成2条IA-32汇编指令:
mov ecx,esi ;把目标对象的引用放进ecx寄存器
call methodAddress ;直接调用methodAddress指向的目标方法
另外:FatherClass既然已经重写了其父类GrandFather中的Viturl方法Func(),而其子类ChildClass却要拒绝接受其重写的Func(),感觉这种继承体系本身就存在一些问题,可以考虑重构一下该继承体系,具体可参考Martin Fowler的《重构-改善既有代码的设计》中Refused Bequest(被拒绝的遗赠)一节。