昨天在某論壇上看到這個問題,覺得有點意思,就貼過來,順便貼下我對該問題的思考。
具體問題是這樣的:
GrandFather類中有一個虛方法
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代碼:
.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 virtual instance void Func() cil managed
IL_0000: ldstr "Father"
} // end of method FatherClass::Func
.method public hidebysig instance void OtherFunc() cil managed
// 代碼大小 7 (0x7)
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()方法。
用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中的方法,可按如下兩種方法:
法一:
class GrandFatherClass
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代碼:
IL_0001: callvirt instance void MyProject.GrandFatherClass::Func()//這裡生成的是callvirt調用指令
從IL代碼中我們可以看到,FatherClass::Func上有應用newslot标示,CLR賦予被申明為newslot的虛方法一個新的methodoffset,是以這裡并沒有覆寫GrandFatherClass::Func(),而在FatherClass的方法表中的方法槽表(Method Slot Table)中,也同時存在兩個Func()槽,一個是繼承而來的,其指向GrandFather的Func()實作,另一個槽執行自身的實作。而ChildClass繼承自FatherClass,是以ChildClass中的方法表中,也有兩個Func()槽。在下圖中,我們也可以看到,這兩個方法在方法表中偏移量,一個為38H,另一個為3CH。
法二:

class GrandFatherClass
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


IL_0001: call instance void MyProject.GrandFatherClass::Func()//這裡生成的是call調用指令
仔細觀察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(被拒絕的遺贈)一節。
本文轉自Silent Void部落格園部落格,原文連結:http://www.cnblogs.com/happyhippy/archive/2006/12/23/601190.html,如需轉載請自行聯系原作者