(原文發表于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。
法二:
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
{
// 代碼大小 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(被拒絕的遺贈)一節。
浙公網安備 33010602011771號