深入探尋.NET委托的幾大秘密
對于委托大家并不陌生,但是對于.NET委托,在本文中作者還是不厭其煩的為大家進行介紹。希望通過本文,能讓大家在使用.NET委托時得心應手。
廢話
我本來以為委托很簡單,本來只想簡簡單單的說說委托背后的東西,委托的使用方法。原本只想解釋一下那句:委托是面向對象的、類型安全的函數(shù)指針。可沒想到最后惹出一堆的事情來,越惹越多,罪過,罪過。本文后面一部分是我在一邊用SOS探索一邊記錄的,寫的非常糟糕,希望您的慧眼能發(fā)現(xiàn)一些有價值的東西,那我就感到無比的榮幸了。
委托前世與今生
大家可能還記得,在C/C++里,我們可以在一個函數(shù)里實現(xiàn)一個算法的骨架,然后在這個函數(shù)的參數(shù)里放一個“鉤子”,使用的時候,利用這個“鉤子”注入一個函數(shù),注入的函數(shù)實現(xiàn)不同算法的不同部分,這樣就可以達到算法骨架重用的目的。而這里所謂的“鉤子”就是“函數(shù)指針”。這個功能很強大啊,但是函數(shù)指針卻有它的劣勢:不是類型安全的、只能“鉤”一個函數(shù)。大家可能都知道微軟對委托的描述:委托是一種面向對象的,類型安全的,可以多播的函數(shù)指針。要理解這句話,我們先來看看用C#的關鍵字delegate聲明的一個委托到底是什么樣的東西:
- namespace Yuyijq.DotNet.Chapter2
- {
- public delegate void MyDelegate(int para);
- }
隱藏在背后的秘密
很簡單的代碼吧,使用ILDasm反編譯一下:
奇怪的是,這么簡單的一行代碼,變成了一個類:類名與委托名一致,這個類繼承自System.MulticastDelegate類,連構造器一起有四個成員??纯次覀內绾问褂眠@個委托:
- public class TestDelegate
- {
- MyDelegate myDelegate;
- public void AssignDelegate()
- {
- this.myDelegate = new MyDelegate(Test);
- }
- public void Test(int para)
- {
- Console.WriteLine("Test Delegate");
- }
- }
編譯后用ILDasm看看結果:
.field private class Yuyijq.DotNet.Chapter2.MyDelegate myDelegate
發(fā)現(xiàn),.NET把委托就當做一個類型,與其他類型一樣對待,現(xiàn)在你明白了上面那句話中說委托是面向對象的函數(shù)指針的意思了吧。
接著看看AssignDelegate反編譯后的代碼:
- .method public hidebysig instance void AssignDelegate() cil managed
- {
- // Code size 19 (0x13)
- .maxstack 8
- //將方法的第一個參數(shù)push到IL的運算棧上(對于一個實例方法來說,比如AssignDelegate,它的第一個參數(shù)就是“this”了)
- IL_0000: ldarg.0
- //這里又把this壓棧了一次,因為下面一條指令中的Test方法是一個實例方法,需要一個this
- IL_0001: ldarg.0
- //ldftn就是把實現(xiàn)它的參數(shù)中的方法的本機代碼的非托管指針push到棧上,在這里你就可以認為是獲取實例方法Test的地址
- IL_0002: ldftn instance void Yuyijq.DotNet.Chapter2.TestDelegate::Test(int32)
- //調用委托的構造器,這個構造器需要兩個參數(shù),一個對象引用,就是第一次壓棧的this,一個方法的地址。
- IL_0008: newobj instance void Yuyijq.DotNet.Chapter2.MyDelegate::.ctor(object,native int)
- IL_000d: stfld class Yuyijq.DotNet.Chapter2.MyDelegate Yuyijq.DotNet.Chapter2.TestDelegate::myDelegate
- IL_0012: ret
- }
通過上面的代碼,我們會發(fā)現(xiàn),將一個實例方法分配給委托時,委托不僅僅引用了方法的地址,還有這個方法所在對象的引用,這里就是所謂的類型安全。
我們再回過頭來看看MyDelegate的繼承鏈:MyDelegate->MulticastDelegate->Delegate。
奇妙的地方
而Delegate中有三個有趣的字段:
- internal object _target;
- internal IntPtr _methodPtr;
- internal IntPtr _methodPtrAux;
對這三個字段做詳細說明
_target
1、如果委托指向的方法是實例方法,則_target的值是指向目標方法所在對象的指針
2、如果委托指向的是靜態(tài)方法,則_target的值是委托實例自身
_methodPtr
1、如果委托指向的方法是實例方法,則_methodPtr的值指向一個JIT Stub(如果這個方法還沒有被JIT編譯,關于JIT Stub會在后面的章節(jié)介紹),或指向該方法JIT后的地址
2、如果委托指向的方法是靜態(tài)方法,則_methodPtr指向的是一個Stub(一段小代碼,這段代碼的作用是_target,然后調用_methodPtrAux指向的方法),而且所有簽名相同的委托,共享這個Stub。為什么要這樣一個Stub?我想是為了讓通過委托調用方法的流程一致吧,不管指向的是實例方法還是靜態(tài)方法,對于外部來說,只需要調用_methodPtr指向的地址,但是對于調用實例方法而言,它需要this,也就是這里的_target,而靜態(tài)方法不需要,為了讓這里的過程一直,CLR會偷偷的在委托指向靜態(tài)方法時插入一小段代碼,用于去掉_target,而直接jmp到_methodPtrAux指向的方法。
_methodPtrAux
1、如果委托指向的是實例方法,則_methodPtrAux就是0。
2、如果委托指向的是靜態(tài)方法,則這時_methodPtrAux起的作用與_mthodPtr在委托指向實例方法的時候是一樣的。
實際上通過反編譯Delegate的代碼發(fā)現(xiàn),Delegate有一個只讀屬性Target,該Target的實現(xiàn)依靠GetTarget方法,該方法的代碼如下:
- internal virtual object GetTarget()
- {
- if (!this._methodPtrAux.IsNull())
- {
- return null;
- }
- return this._target;
- }
實了當委托指向靜態(tài)方法時,Target屬性為null。
我們來自己動手,分析一下上面的結論是否正確。
_target和_methodPtr真的如上面所說的么?何不自己動手看看。
建立一個Console類型的工程,在項目屬性的“調試(Debug)”選項卡里選中“允許非托管代碼調試(Enable unmanaged code debuging)”。
- namespace Yuyijq.DotNet.Chapter2
- {
- public delegate void MyDelegate(int para);
- public class TestDelegate
- {
- public void Test(int para)
- {
- Console.WriteLine("Test Delegate");
- }
- public void CallByDelegate()
- {
- MyDelegate myDelegate = new MyDelegate(this.Test);
- myDelegate(5);
- }
- static void Main()
- {
- TestDelegate test = new TestDelegate();
- test.CallByDelegate();
- }
- }
- }
上面是作為實驗的代碼。
在CallByDelegate方法的第二行設置斷點
F5執(zhí)行,命中斷電后,在Visual Studio的立即窗口(Immediate Window)里輸入如下命令(菜單欄->調試(Debug)->立即窗口(Immediate)):
- //.load sos.dll用于加載SOS.dll擴展
- .load sos.dll
- extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
- //Dump Stack Objects的縮寫,輸出棧中的所有對象
- //該命令的輸出有三列,第二列Object就是該對象在內存中的地址
- !dso
- PDB symbol for mscorwks.dll not loaded
- OS Thread Id: 0x1588 (5512)
- ESP/REG Object Name
- 0037ec10 019928a4 Yuyijq.DotNet.Chapter2.TestDelegate
- 0037ed50 019928a4 Yuyijq.DotNet.Chapter2.TestDelegate
- 0037ed5c 019928b0 Yuyijq.DotNet.Chapter2.MyDelegate
- 0037ed60 019928b0 Yuyijq.DotNet.Chapter2.MyDelegate
- 0037ef94 019928b0 Yuyijq.DotNet.Chapter2.MyDelegate
- 0037ef98 019928b0 Yuyijq.DotNet.Chapter2.MyDelegate
- 0037ef9c 019928a4 Yuyijq.DotNet.Chapter2.TestDelegate
- 0037efe0 019928a4 Yuyijq.DotNet.Chapter2.TestDelegate
- 0037efe4 019928a4 Yuyijq.DotNet.Chapter2.TestDelegate
- //do命令為Dump Objects縮寫,參數(shù)為對象地址,輸出該對象的一些信息
- !do 019928b0
- Name: Yuyijq.DotNet.Chapter2.MyDelegate
- MethodTable: 00263100
- EEClass: 002617e8
- Size: 32(0x20) bytes
- (E:\Study\Demo\Demo\bin\Debug\Demo.exe)
- //該對象的一些字段
- Fields:
- MT Field Offset Type VT Attr Value Name
- 704b84dc 40000ff 4 System.Object 0 instance 019928a4 _target
- 704bd0ac 4000100 8 ...ection.MethodBase 0 instance 00000000 _methodBase
- 704bb188 4000101 c System.IntPtr 1 instance 0026C018 _methodPtr
- 704bb188 4000102 10 System.IntPtr 1 instance 00000000 _methodPtrAux
- 704b84dc 400010c 14 System.Object 0 instance 00000000 _invocationList
- 704bb188 400010d 18 System.IntPtr 1 instance 00000000 _invocationCount
在最后Fields一部分,我們看到了_target喝_methodPtr,_target的值為019928a4,看看上面!dso命令的輸出,這個不就是Yuyijq.DotNet.Chapter2.TestDelegate實例的內存地址么。
在上面的!do命令的輸出中,我們看到了MethodTable:00263100,這就是該對象的方法表地址(關于方法表更詳細的討論會在后面的章節(jié)介紹到,現(xiàn)在你只要把他看做一個記錄對象所有方法的列表就行了,該列表里每一個條目就是一個方法)?,F(xiàn)在我們要看看Yuyijq.DotNet.Chapter2.TestDelegate..Test方法的內存地址,看起是否與_methodPtr的值是一致的,那么首先就要獲得Yuyijq.DotNet.Chapter2.TestDelegate.的實例中MethodTable的值:
- !do 019928a4
- Name: Yuyijq.DotNet.Chapter2.TestDelegate
- MethodTable: 00263048
- EEClass: 002612f8
- Size: 12(0xc) bytes
- (E:\Study\Demo\Demo\bin\Debug\Demo.exe)
- Fields:
- None
現(xiàn)在知道了其方法表的值為00263048,然后使用下面的命令找到Yuyijq.DotNet.Chapter2.TestDelegate..Test方法的地址:
- !dumpmt -md 00263048
- EEClass: 002612f8
- Module: 00262c5c
- Name: Yuyijq.DotNet.Chapter2.TestDelegate
- mdToken: 02000003 (E:\Study\Demo\Demo\bin\Debug\Demo.exe)
- BaseSize: 0xc
- ComponentSize: 0x0
- Number of IFaces in IFaceMap: 0
- Slots in VTable: 9
- --------------------------------------
- MethodDesc Table
- Entry MethodDesc JIT Name
- .......
- 0026c010 00262ffc NONE Yuyijq.DotNet.Chapter2.TestDelegate.AssignDelegate()
- 0026c018 0026300c NONE Yuyijq.DotNet.Chapter2.TestDelegate.Test(Int32)
- ......
Entry這一列就是一個JIT Stub??纯?,果然與_methodPtr的是一致的,因為這時Test方法還沒有經(jīng)過JIT(JIT列為NONE),所以_methodPtr指向的是這里的JIT Stub。
如果給委托綁定一個靜態(tài)方法呢?現(xiàn)在我們把Test方法改為靜態(tài)的,那實例化委托的時候,就不能用this.Test了,而應該用TestDelegate.Test。還是在原位置設置斷點,使用與上面相同的命令,查看_target與_methodPtr的值。
- MT Field Offset Type VT Attr Value Name
- 704b84dc 40000ff 4 System.Object 0 instance 01e928b0 _target
- 704bb188 4000101 c System.IntPtr 1 instance 007809C4 _methodPtr
- 704bb188 4000102 10 System.IntPtr 1 instance 0025C018 _methodPtrAux
你會發(fā)現(xiàn)這里的_target字段的值就是MyDelegate的實例myDelegate的地址。然后我們通過上面的方法,找到Test方法的地址,發(fā)現(xiàn)_methodPtrAux的值與該值是相同的。
實際上你還可以再編寫一個與MyDelegate相同簽名的委托,然后也指向一個靜態(tài)方法,使用相同的方法查看該委托的_methodPtr的值,你會發(fā)現(xiàn)這個新委托與MyDelegate的_methodPtr的值是一致的。
剛才不是說這個時候_methodPtr指向的是一個Stub么,既然如此那我們反匯編一下代碼:
- !u 007809C4
- Unmanaged code
- 007809C4 8BC1 mov eax,ecx
- 007809C6 8BCA mov ecx,edx
- 007809C8 83C010 add eax,10h
- 007809CB FF20 jmp dword ptr [eax]
- ........
.Net里JIT的方法的調用約定是Fast Call,對于Fast Call來說,方法的前兩個參數(shù)會放在ECX和EDX兩個寄存器中。那么mov eax,ecx實際上就是將_target傳遞給eax,再看看
704bb188 4000102 10 System.IntPtr 1 instance 0025C018 _methodPtrAux
_methodPtrAux的偏移是10,這里的add eax,10h就是將eax指向_methodPtrAux,然后jmp dword ptr[eax]就是跳轉到_methodPtrAux所指向的地址了,就是委托指向的那個靜態(tài)方法。
通過委托調用方法
如何通過委托調用方法呢:
- public void CallByDelegate()
- MyDelegate myDelegate = new MyDelegate(this.Test);
- myDelegate(5);
- }
再來看看其對應的IL代碼:
- .method public hidebysig instance void CallByDelegate() cil managed
- { // Code size 21 (0x15)
- .maxstack 3
- .locals init ([0] class Yuyijq.DotNet.Chapter2.MyDelegate myDelegate)
- IL_0000: ldarg.0
- IL_0001: ldftn instance void Yuyijq.DotNet.Chapter2.TestDelegate::Test(int32)
- IL_0007: newobj instance void Yuyijq.DotNet.Chapter2.MyDelegate::.ctor(object, native int)
- IL_000c: stloc.0
- IL_000d: ldloc.0
- IL_000e: ldc.i4.5
- IL_000f: callvirt instance void Yuyijq.DotNet.Chapter2.MyDelegate::Invoke(int32)
- IL_0014: ret
- }
前面的代碼我們已經(jīng)熟悉,最關鍵的就是
- callvirt instance void Yuyijq.DotNet.Chapter2.MyDelegate::Invoke(int32)
我們發(fā)現(xiàn),通過委托調用方法,實際上就是調用委托的Invoke方法。
多播的委托
好了,既然已經(jīng)解釋了面向對象和類型安全,那么說委托是多播的咋解釋?
你可能已經(jīng)發(fā)現(xiàn),MyDelegate繼承自MulticastDelegate,看這個名字貌似有點意思了。來看看下面這兩行代碼:
- MyDelegate myDelegate = new MyDelegate(this.Test);
- myDelegate += new MyDelegate(this.Test1);
通過IL我們可以發(fā)現(xiàn),這里的+=最后就是調用System.Delegate的Combine方法。而Combine的真正實現(xiàn)時在MulticastDelegate的CombineImpl方法中。在MulticastDelegate中有一個_invocationList字段,從CombineImpl中可以看出這個字段是一個object[]類型的,而委托鏈就放在這個數(shù)組里。
.NET委托后記
文章是想到哪兒寫到哪兒,寫的比較亂,也比較匆忙。非常抱歉。對于中間那段奇妙的事情,我原來真的不知道,我一直以為當委托指向一個靜態(tài)方法時,_target指向null就完事兒了,沒想到還有這么一番景象。看來很多東西還是不能想當然,親身嘗試一下才知道真實的情況。
原文標題:探索.Net中的委托
鏈接:http://www.cnblogs.com/yuyijq/archive/2009/10/14/1583251.html
【編輯推薦】