最近一直對.net framework中,虛方法的調用是如何實現這個問題有些疑惑,在看了Essential .Net 關於Method的那一章和Artech推薦的文章Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects以後,還是一知半解,有些疑惑得不到答案。主要有這些:
父類定義的非虛方法是否在子類中有拷貝?
虛方法是如何實現多態的?
子類繼承父類的虛方法實現是否和繼承非虛方法機制相同?
如果子類隱藏了父類的虛方法,這又是怎樣實現的?
當然問題不止這麼多,關於接口方面還有很多很多疑惑,不過時間有限,一下也沒辦法全部弄清楚, 有時間慢慢研究。我主要使用Windbg工具來跟蹤調試,關於這個工具如何使用,Google一下就會有很多了 。
這些都是我自己研究加上參考資料所得,如果有不對的地方,希望大家討論指出。
首先看下面這段代碼:
public class Base
{
public virtual void VirtualFun1()
{
Console.WriteLine("Base.VirtualFun1");
}
public void NoneVirtualFun1()
{
System.Console.WriteLine("Base.NoneVirtualFun1");
}
public virtual void VirtualFun2()
{
System.Console.WriteLine("Base.VirtualFun2");
}
public virtual void VirtualFun3()
{
System.Console.WriteLine("Base.VirtualFun3");
}
}
public class Derived : Base
{
public override void VirtualFun1()
{
Console.WriteLine("Derived.VirtualFun1");
}
public new virtual void VirtualFun2()
{
System.Console.WriteLine("Derived.VirtualFun2");
}
public virtual void VirtualFun4()
{
System.Console.WriteLine("Derived.VirtualFun4");
}
}
Base類是基類,它包含三個虛方法VirtualFun1, VirtuaFun2, VirtualFun3和一個非虛方法 NoneVirtualFun1。
Derived繼承Base類,它重寫了VirtualFun1虛方法,隱藏了Base類的VirtualFun2虛方法,然後又增加 了VirtualFun4虛方法。
看看一個Base類的實例在內存中是怎樣排布的:
Object Ref表示某Base實例的引用,它指向在GC Heap中分配的Base對象,這個對象可以分為三部分: 同步塊索引、類型指針和字段。主要來關注類型指針,它指向該類型的Method Table,這其實是在Load Heap中分配的Type類型對象,所有該類型的實例的類型指針都指向同一個Method Table(這裡表示所有 Base對象的類型指針都指向同一個Method Table)。
Method Table裡面包含很多信息,這裡關注有關Method這一區域,(如果想了解更詳細的method table,請參考上面的文章)。
根據在Method Table裡的信息,可以知道它包含9個Method(其實應該有個字段標示有多少個虛方法, 這裡就沒畫了)。接下來就是這些method,它分為兩部分,前面一部分是所有的虛方法,後面的是非虛方 法。因為所有的類型都是繼承自System.Object類,所以前四個方法是Object類的虛方法(ToString, Equals, GetHashCode, Finalize),接著是Base類定義的三個虛方法(VirtualFun1, VirtualFun2, VirtualFun3),最後是Base類的非虛方法NoneVirtualFun1以及默認的構造函數。下面再來看看Derived 類型的Method Table:
仔細對比一下這兩個Method Table,可以發現這樣幾個特點:
Base類中的所有虛方法在Derived類的Method Table中一一對應
Base類中的所有非虛方法在Derived類中的Method Table並沒有拷貝(這一點回答了上面的第一個問題 )
Derived類新增的虛方法都添加到繼承自Base類的虛方法的後面
如果Derived類override Base類的虛方法,它就將該方法指向自身的實現
如果Derived類使用new關鍵字隱藏了Base類虛方法的實現,它就相當於增加了一個虛方法,而不是覆 蓋。
下面看看調用虛方法時如何實現多態,比如有這樣一段代碼
Base b = new Derived();
b.VirtualFun1();
編譯後在我的機器上會生成這樣的匯編代碼:
mov ecx, esi
mov eax, dword ptr[ecx]
call dword ptr [eax + 3ch]
現在來解釋這幾句代碼:mov ecx, esi 是將新構造的對象的地址保存在ecx寄存器中; mov eax, dword ptr[ecx] 表示ecx的值是一個指針(根據上面的圖可以知道對象的頭4個字節保存的是method table的地址),它將method table的地址保存到eax寄存器中,最後call dword ptr[eax + 3ch]。3ch表 示偏移量,它表示該方法相對於該method table的偏址,是在該類型加載到load heap以後確定的。這樣 ,由method table的地址加上method相對與method table的偏移量,就可以唯一確定一個方法。
這樣在調用b.VirtualFun1(); 時,由於b是Derived類的實例,所以根據它指向的托管對象找到的 method table是Derived類型的method table,就能正確調用該方法。因為Derived類中override了 VirtualFun1這個虛方法,所以調用的是Derived類的實現,而如果沒有override基類的虛方法,它就指向 基類的該方法的實現。
由此可以看出,CLR實現虛方法的機制主要是通過類型的method table加上該虛方法相對於method table的偏移量來確定調用具體方法的。一個虛方法在整個繼承體系所有類型對應的method table中的偏 移量是固定的,比如VirtualFunc1在Base類型的method table中的偏移量是3ch,它在Derived類型的 method table中的偏移量也是3ch,如果還有繼承自Derived類的類,也是同樣,利用這種機制就實現了多 態。
結論
每個類型對應一個Method Table
子類的Method Table中包含父類的所有虛方法,而不包含父類的非虛方法
CLR根據對象找到它對應類型的method table,然後根據該虛方法在method table中的偏移量實現多態 調用。