深入探尋.NET委托的幾大秘密
對(duì)于委托大家并不陌生,但是對(duì)于.NET委托,在本文中作者還是不厭其煩的為大家進(jìn)行介紹。希望通過(guò)本文,能讓大家在使用.NET委托時(shí)得心應(yīng)手。
廢話
我本來(lái)以為委托很簡(jiǎn)單,本來(lái)只想簡(jiǎn)簡(jiǎn)單單的說(shuō)說(shuō)委托背后的東西,委托的使用方法。原本只想解釋一下那句:委托是面向?qū)ο蟮?、?lèi)型安全的函數(shù)指針。可沒(méi)想到最后惹出一堆的事情來(lái),越惹越多,罪過(guò),罪過(guò)。本文后面一部分是我在一邊用SOS探索一邊記錄的,寫(xiě)的非常糟糕,希望您的慧眼能發(fā)現(xiàn)一些有價(jià)值的東西,那我就感到無(wú)比的榮幸了。
委托前世與今生
大家可能還記得,在C/C++里,我們可以在一個(gè)函數(shù)里實(shí)現(xiàn)一個(gè)算法的骨架,然后在這個(gè)函數(shù)的參數(shù)里放一個(gè)“鉤子”,使用的時(shí)候,利用這個(gè)“鉤子”注入一個(gè)函數(shù),注入的函數(shù)實(shí)現(xiàn)不同算法的不同部分,這樣就可以達(dá)到算法骨架重用的目的。而這里所謂的“鉤子”就是“函數(shù)指針”。這個(gè)功能很強(qiáng)大啊,但是函數(shù)指針卻有它的劣勢(shì):不是類(lèi)型安全的、只能“鉤”一個(gè)函數(shù)。大家可能都知道微軟對(duì)委托的描述:委托是一種面向?qū)ο蟮模?lèi)型安全的,可以多播的函數(shù)指針。要理解這句話,我們先來(lái)看看用C#的關(guān)鍵字delegate聲明的一個(gè)委托到底是什么樣的東西:
- namespace Yuyijq.DotNet.Chapter2
- {
- public delegate void MyDelegate(int para);
- }
隱藏在背后的秘密
很簡(jiǎn)單的代碼吧,使用ILDasm反編譯一下:
奇怪的是,這么簡(jiǎn)單的一行代碼,變成了一個(gè)類(lèi):類(lèi)名與委托名一致,這個(gè)類(lèi)繼承自System.MulticastDelegate類(lèi),連構(gòu)造器一起有四個(gè)成員。看看我們?nèi)绾问褂眠@個(gè)委托:
- public class TestDelegate
- {
- MyDelegate myDelegate;
- public void AssignDelegate()
- {
- this.myDelegate = new MyDelegate(Test);
- }
- public void Test(int para)
- {
- Console.WriteLine("Test Delegate");
- }
- }
編譯后用ILDasm看看結(jié)果:
.field private class Yuyijq.DotNet.Chapter2.MyDelegate myDelegate
發(fā)現(xiàn),.NET把委托就當(dāng)做一個(gè)類(lèi)型,與其他類(lèi)型一樣對(duì)待,現(xiàn)在你明白了上面那句話中說(shuō)委托是面向?qū)ο蟮暮瘮?shù)指針的意思了吧。
接著看看AssignDelegate反編譯后的代碼:
- .method public hidebysig instance void AssignDelegate() cil managed
- {
- // Code size 19 (0x13)
- .maxstack 8
- //將方法的第一個(gè)參數(shù)push到IL的運(yùn)算棧上(對(duì)于一個(gè)實(shí)例方法來(lái)說(shuō),比如AssignDelegate,它的第一個(gè)參數(shù)就是“this”了)
- IL_0000: ldarg.0
- //這里又把this壓棧了一次,因?yàn)橄旅嬉粭l指令中的Test方法是一個(gè)實(shí)例方法,需要一個(gè)this
- IL_0001: ldarg.0
- //ldftn就是把實(shí)現(xiàn)它的參數(shù)中的方法的本機(jī)代碼的非托管指針push到棧上,在這里你就可以認(rèn)為是獲取實(shí)例方法Test的地址
- IL_0002: ldftn instance void Yuyijq.DotNet.Chapter2.TestDelegate::Test(int32)
- //調(diào)用委托的構(gòu)造器,這個(gè)構(gòu)造器需要兩個(gè)參數(shù),一個(gè)對(duì)象引用,就是第一次壓棧的this,一個(gè)方法的地址。
- 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
- }
通過(guò)上面的代碼,我們會(huì)發(fā)現(xiàn),將一個(gè)實(shí)例方法分配給委托時(shí),委托不僅僅引用了方法的地址,還有這個(gè)方法所在對(duì)象的引用,這里就是所謂的類(lèi)型安全。
我們?cè)倩剡^(guò)頭來(lái)看看MyDelegate的繼承鏈:MyDelegate->MulticastDelegate->Delegate。
奇妙的地方
而Delegate中有三個(gè)有趣的字段:
- internal object _target;
- internal IntPtr _methodPtr;
- internal IntPtr _methodPtrAux;
對(duì)這三個(gè)字段做詳細(xì)說(shuō)明
_target
1、如果委托指向的方法是實(shí)例方法,則_target的值是指向目標(biāo)方法所在對(duì)象的指針
2、如果委托指向的是靜態(tài)方法,則_target的值是委托實(shí)例自身
_methodPtr
1、如果委托指向的方法是實(shí)例方法,則_methodPtr的值指向一個(gè)JIT Stub(如果這個(gè)方法還沒(méi)有被JIT編譯,關(guān)于JIT Stub會(huì)在后面的章節(jié)介紹),或指向該方法JIT后的地址
2、如果委托指向的方法是靜態(tài)方法,則_methodPtr指向的是一個(gè)Stub(一段小代碼,這段代碼的作用是_target,然后調(diào)用_methodPtrAux指向的方法),而且所有簽名相同的委托,共享這個(gè)Stub。為什么要這樣一個(gè)Stub?我想是為了讓通過(guò)委托調(diào)用方法的流程一致吧,不管指向的是實(shí)例方法還是靜態(tài)方法,對(duì)于外部來(lái)說(shuō),只需要調(diào)用_methodPtr指向的地址,但是對(duì)于調(diào)用實(shí)例方法而言,它需要this,也就是這里的_target,而靜態(tài)方法不需要,為了讓這里的過(guò)程一直,CLR會(huì)偷偷的在委托指向靜態(tài)方法時(shí)插入一小段代碼,用于去掉_target,而直接jmp到_methodPtrAux指向的方法。
_methodPtrAux
1、如果委托指向的是實(shí)例方法,則_methodPtrAux就是0。
2、如果委托指向的是靜態(tài)方法,則這時(shí)_methodPtrAux起的作用與_mthodPtr在委托指向?qū)嵗椒ǖ臅r(shí)候是一樣的。
實(shí)際上通過(guò)反編譯Delegate的代碼發(fā)現(xiàn),Delegate有一個(gè)只讀屬性Target,該Target的實(shí)現(xiàn)依靠GetTarget方法,該方法的代碼如下:
- internal virtual object GetTarget()
- {
- if (!this._methodPtrAux.IsNull())
- {
- return null;
- }
- return this._target;
- }
實(shí)了當(dāng)委托指向靜態(tài)方法時(shí),Target屬性為null。
我們來(lái)自己動(dòng)手,分析一下上面的結(jié)論是否正確。
_target和_methodPtr真的如上面所說(shuō)的么?何不自己動(dòng)手看看。
建立一個(gè)Console類(lèi)型的工程,在項(xiàng)目屬性的“調(diào)試(Debug)”選項(xiàng)卡里選中“允許非托管代碼調(diào)試(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();
- }
- }
- }
上面是作為實(shí)驗(yàn)的代碼。
在CallByDelegate方法的第二行設(shè)置斷點(diǎn)
F5執(zhí)行,命中斷電后,在Visual Studio的立即窗口(Immediate Window)里輸入如下命令(菜單欄->調(diào)試(Debug)->立即窗口(Immediate)):
- //.load sos.dll用于加載SOS.dll擴(kuò)展
- .load sos.dll
- extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
- //Dump Stack Objects的縮寫(xiě),輸出棧中的所有對(duì)象
- //該命令的輸出有三列,第二列Object就是該對(duì)象在內(nèi)存中的地址
- !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縮寫(xiě),參數(shù)為對(duì)象地址,輸出該對(duì)象的一些信息
- !do 019928b0
- Name: Yuyijq.DotNet.Chapter2.MyDelegate
- MethodTable: 00263100
- EEClass: 002617e8
- Size: 32(0x20) bytes
- (E:\Study\Demo\Demo\bin\Debug\Demo.exe)
- //該對(duì)象的一些字段
- 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命令的輸出,這個(gè)不就是Yuyijq.DotNet.Chapter2.TestDelegate實(shí)例的內(nèi)存地址么。
在上面的!do命令的輸出中,我們看到了MethodTable:00263100,這就是該對(duì)象的方法表地址(關(guān)于方法表更詳細(xì)的討論會(huì)在后面的章節(jié)介紹到,現(xiàn)在你只要把他看做一個(gè)記錄對(duì)象所有方法的列表就行了,該列表里每一個(gè)條目就是一個(gè)方法)。現(xiàn)在我們要看看Yuyijq.DotNet.Chapter2.TestDelegate..Test方法的內(nèi)存地址,看起是否與_methodPtr的值是一致的,那么首先就要獲得Yuyijq.DotNet.Chapter2.TestDelegate.的實(shí)例中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,然后使用下面的命令找到Y(jié)uyijq.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這一列就是一個(gè)JIT Stub??纯?,果然與_methodPtr的是一致的,因?yàn)檫@時(shí)Test方法還沒(méi)有經(jīng)過(guò)JIT(JIT列為NONE),所以_methodPtr指向的是這里的JIT Stub。
如果給委托綁定一個(gè)靜態(tài)方法呢?現(xiàn)在我們把Test方法改為靜態(tài)的,那實(shí)例化委托的時(shí)候,就不能用this.Test了,而應(yīng)該用TestDelegate.Test。還是在原位置設(shè)置斷點(diǎn),使用與上面相同的命令,查看_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
你會(huì)發(fā)現(xiàn)這里的_target字段的值就是MyDelegate的實(shí)例myDelegate的地址。然后我們通過(guò)上面的方法,找到Test方法的地址,發(fā)現(xiàn)_methodPtrAux的值與該值是相同的。
實(shí)際上你還可以再編寫(xiě)一個(gè)與MyDelegate相同簽名的委托,然后也指向一個(gè)靜態(tài)方法,使用相同的方法查看該委托的_methodPtr的值,你會(huì)發(fā)現(xiàn)這個(gè)新委托與MyDelegate的_methodPtr的值是一致的。
剛才不是說(shuō)這個(gè)時(shí)候_methodPtr指向的是一個(gè)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的方法的調(diào)用約定是Fast Call,對(duì)于Fast Call來(lái)說(shuō),方法的前兩個(gè)參數(shù)會(huì)放在ECX和EDX兩個(gè)寄存器中。那么mov eax,ecx實(shí)際上就是將_target傳遞給eax,再看看
704bb188 4000102 10 System.IntPtr 1 instance 0025C018 _methodPtrAux
_methodPtrAux的偏移是10,這里的add eax,10h就是將eax指向_methodPtrAux,然后jmp dword ptr[eax]就是跳轉(zhuǎn)到_methodPtrAux所指向的地址了,就是委托指向的那個(gè)靜態(tài)方法。
通過(guò)委托調(diào)用方法
如何通過(guò)委托調(diào)用方法呢:
- public void CallByDelegate()
- MyDelegate myDelegate = new MyDelegate(this.Test);
- myDelegate(5);
- }
再來(lái)看看其對(duì)應(yīng)的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)熟悉,最關(guān)鍵的就是
- callvirt instance void Yuyijq.DotNet.Chapter2.MyDelegate::Invoke(int32)
我們發(fā)現(xiàn),通過(guò)委托調(diào)用方法,實(shí)際上就是調(diào)用委托的Invoke方法。
多播的委托
好了,既然已經(jīng)解釋了面向?qū)ο蠛皖?lèi)型安全,那么說(shuō)委托是多播的咋解釋?zhuān)?/P>
你可能已經(jīng)發(fā)現(xiàn),MyDelegate繼承自MulticastDelegate,看這個(gè)名字貌似有點(diǎn)意思了。來(lái)看看下面這兩行代碼:
- MyDelegate myDelegate = new MyDelegate(this.Test);
- myDelegate += new MyDelegate(this.Test1);
通過(guò)IL我們可以發(fā)現(xiàn),這里的+=最后就是調(diào)用System.Delegate的Combine方法。而Combine的真正實(shí)現(xiàn)時(shí)在MulticastDelegate的CombineImpl方法中。在MulticastDelegate中有一個(gè)_invocationList字段,從CombineImpl中可以看出這個(gè)字段是一個(gè)object[]類(lèi)型的,而委托鏈就放在這個(gè)數(shù)組里。
.NET委托后記
文章是想到哪兒寫(xiě)到哪兒,寫(xiě)的比較亂,也比較匆忙。非常抱歉。對(duì)于中間那段奇妙的事情,我原來(lái)真的不知道,我一直以為當(dāng)委托指向一個(gè)靜態(tài)方法時(shí),_target指向null就完事兒了,沒(méi)想到還有這么一番景象。看來(lái)很多東西還是不能想當(dāng)然,親身嘗試一下才知道真實(shí)的情況。
原文標(biāo)題:探索.Net中的委托
鏈接:http://www.cnblogs.com/yuyijq/archive/2009/10/14/1583251.html
【編輯推薦】