探索.Net中的委托

作者: 橫刀天笑  來源: 博客園  發布時間: 2009-10-14 15:53  閱讀: 1409 次  推薦: 1   原文鏈接   [收藏]  

廢話

我本來以為委托很簡單,本來只想簡簡單單的說說委托背后的東西,委托的使用方法。原本只想解釋一下那句:委托是面向對象的、類型安全的函數指針。可沒想到最后惹出一堆的事情來,越惹越多,罪過,罪過。本文后面一部分是我在一邊用SOS探索一邊記錄的,寫的非常糟糕,希望您的慧眼能發現一些有價值的東西,那我就感到無比的榮幸了。

委托前世與今生

大家可能還記得,在C/C++里,我們可以在一個函數里實現一個算法的骨架,然后在這個函數的參數里放一個“鉤子”,使用的時候,利用這個“鉤子”注入一個函數,注入的函數實現不同算法的不同部分,這樣就可以達到算法骨架重用的目的。而這里所謂的“鉤子”就是“函數指針”。這個功能很強大啊,但是函數指針卻有它的劣勢:不是類型安全的、只能“鉤”一個函數。大家可能都知道微軟對委托的描述:委托是一種面向對象的,類型安全的,可以多播的函數指針。要理解這句話,我們先來看看用C#的關鍵字delegate聲明的一個委托到底是什么樣的東西:

   1: namespace Yuyijq.DotNet.Chapter2
   2: {
   3:     public delegate void MyDelegate(int para);
   4: }

 

隱藏在背后的秘密

很簡單的代碼吧,使用ILDasm反編譯一下:

wps_clip_image-0

奇怪的是,這么簡單的一行代碼,變成了一個類:類名與委托名一致,這個類繼承自System.MulticastDelegate類,連構造器一起有四個成員。看看我們如何使用這個委托:

   1: public class TestDelegate
   2: {
   3:     MyDelegate myDelegate;
   4:  
   5:     public void AssignDelegate()
   6:     {
   7:         this.myDelegate = new MyDelegate(Test);
   8:     }
   9:  
  10:     public void Test(int para)
  11:     {
  12:         Console.WriteLine("Test Delegate");
  13:     }
  14: }

編譯后用ILDasm看看結果:

.field private class Yuyijq.DotNet.Chapter2.MyDelegate myDelegate

發現,.Net把委托就當做一個類型,與其他類型一樣對待,現在你明白了上面那句話中說委托是面向對象的函數指針的意思了吧。

接著看看AssignDelegate反編譯后的代碼:

   1: .method public hidebysig instance void  AssignDelegate() cil managed
   2: {
   3:   // Code size       19 (0x13)
   4:   .maxstack  8
   5: //將方法的第一個參數push到IL的運算棧上(對于一個實例方法來說,比如AssignDelegate,它的第一個參數就是“this”了)
   6:   IL_0000:  ldarg.0
   7: //這里又把this壓棧了一次,因為下面一條指令中的Test方法是一個實例方法,需要一個this
   8:   IL_0001:  ldarg.0
   9: //ldftn就是把實現它的參數中的方法的本機代碼的非托管指針push到棧上,在這里你就可以認為是獲取實例方法Test的地址
  10:   IL_0002:  ldftn instance void Yuyijq.DotNet.Chapter2.TestDelegate::Test(int32)
  11: //調用委托的構造器,這個構造器需要兩個參數,一個對象引用,就是第一次壓棧的this,一個方法的地址。
  12:   IL_0008:  newobj instance void Yuyijq.DotNet.Chapter2.MyDelegate::.ctor(object,native int)
  13:   IL_000d:  stfld class Yuyijq.DotNet.Chapter2.MyDelegate Yuyijq.DotNet.Chapter2.TestDelegate::myDelegate
  14:   IL_0012:  ret
  15: }

通過上面的代碼,我們會發現,將一個實例方法分配給委托時,委托不僅僅引用了方法的地址,還有這個方法所在對象的引用,這里就是所謂的類型安全。

我們再回過頭來看看MyDelegate的繼承鏈:MyDelegate->MulticastDelegate->Delegate。

奇妙的地方

而Delegate中有三個有趣的字段:

Internal object _target;

Internal IntPtr _methodPtr;

Internal IntPtr _methodPtr;

對這三個字段做詳細說明

_target

1、如果委托指向的方法是實例方法,則_target的值是指向目標方法所在對象的指針

2、如果委托指向的是靜態方法,則_target的值是委托實例自身

_methodPtr

1、如果委托指向的方法是實例方法,則_methodPtr的值指向一個JIT Stub(如果這個方法還沒有被JIT編譯,關于JIT Stub會在后面的章節介紹),或指向該方法JIT后的地址

2、如果委托指向的方法是靜態方法,則_methodPtr指向的是一個Stub(一段小代碼,這段代碼的作用是_target,然后調用_methodPtrAux指向的方法),而且所有簽名相同的委托,共享這個Stub。為什么要這樣一個Stub?我想是為了讓通過委托調用方法的流程一致吧,不管指向的是實例方法還是靜態方法,對于外部來說,只需要調用_methodPtr指向的地址,但是對于調用實例方法而言,它需要this,也就是這里的_target,而靜態方法不需要,為了讓這里的過程一直,CLR會偷偷的在委托指向靜態方法時插入一小段代碼,用于去掉_target,而直接jmp到_methodPtrAux指向的方法。

_methodPtrAux

1、如果委托指向的是實例方法,則_methodPtrAux就是0。

2、如果委托指向的是靜態方法,則這時_methodPtrAux起的作用與_mthodPtr在委托指向實例方法的時候是一樣的。

實際上通過反編譯Delegate的代碼發現,Delegate有一個只讀屬性Target,該Target的實現依靠GetTarget方法,該方法的代碼如下:

   1: internal virtual object GetTarget()
   2: {
   3:     if (!this._methodPtrAux.IsNull())
   4:     {
   5:         return null;
   6:     }
   7:     return this._target;
   8: }

實了當委托指向靜態方法時,Target屬性為null。

我們來自己動手,分析一下上面的結論是否正確。

_target和_methodPtr真的如上面所說的么?何不自己動手看看。

建立一個Console類型的工程,在項目屬性的“調試(Debug)”選項卡里選中“允許非托管代碼調試(Enable unmanaged code debuging)”。

   1: namespace Yuyijq.DotNet.Chapter2
   2: {
   3:     public delegate void MyDelegate(int para);
   4:     public class TestDelegate
   5:     {
   6:         public void Test(int para)
   7:         {
   8:             Console.WriteLine("Test Delegate");
   9:         }
  10:         public void CallByDelegate()
  11:         {
  12:             MyDelegate myDelegate = new MyDelegate(this.Test);
  13:             myDelegate(5);
  14:         }
  15:  
  16:         static void Main()
  17:         {
  18:             TestDelegate test = new TestDelegate();
  19:             test.CallByDelegate();
  20:         }
  21:     }
  22: }

上面是作為實驗的代碼。

在CallByDelegate方法的第二行設置斷點

F5執行,命中斷電后,在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縮寫,參數為對象地址,輸出該對象的一些信息

!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,這就是該對象的方法表地址(關于方法表更詳細的討論會在后面的章節介紹到,現在你只要把他看做一個記錄對象所有方法的列表就行了,該列表里每一個條目就是一個方法)。現在我們要看看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

現在知道了其方法表的值為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方法還沒有經過JIT(JIT列為NONE),所以_methodPtr指向的是這里的JIT Stub。

如果給委托綁定一個靜態方法呢?現在我們把Test方法改為靜態的,那實例化委托的時候,就不能用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

你會發現這里的_target字段的值就是MyDelegate的實例myDelegate的地址。然后我們通過上面的方法,找到Test方法的地址,發現_methodPtrAux的值與該值是相同的。

實際上你還可以再編寫一個與MyDelegate相同簽名的委托,然后也指向一個靜態方法,使用相同的方法查看該委托的_methodPtr的值,你會發現這個新委托與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來說,方法的前兩個參數會放在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所指向的地址了,就是委托指向的那個靜態方法。

通過委托調用方法

如何通過委托調用方法呢:

   1: public void CallByDelegate()
   2: {
   3:    MyDelegate myDelegate = new MyDelegate(this.Test);
   4:  
   5:    myDelegate(5);
   6: }

再來看看其對應的IL代碼:

   1: .method public hidebysig instance void  CallByDelegate() cil managed
   2: {
   3:   // Code size       21 (0x15)
   4:   .maxstack  3
   5:   .locals init ([0] class Yuyijq.DotNet.Chapter2.MyDelegate myDelegate)
   6:   IL_0000:  ldarg.0
   7:   IL_0001:  ldftn instance void Yuyijq.DotNet.Chapter2.TestDelegate::Test(int32)
   8:   IL_0007:  newobj instance void Yuyijq.DotNet.Chapter2.MyDelegate::.ctor(object, native int)
   9:   IL_000c:  stloc.0
  10:   IL_000d:  ldloc.0
  11:   IL_000e:  ldc.i4.5
  12:   IL_000f:  callvirt   instance void Yuyijq.DotNet.Chapter2.MyDelegate::Invoke(int32)
  13:   IL_0014:  ret
  14: }

前面的代碼我們已經熟悉,最關鍵的就是

callvirt instance void Yuyijq.DotNet.Chapter2.MyDelegate::Invoke(int32)

我們發現,通過委托調用方法,實際上就是調用委托的Invoke方法。

多播的委托

好了,既然已經解釋了面向對象和類型安全,那么說委托是多播的咋解釋?

你可能已經發現,MyDelegate繼承自MulticastDelegate,看這個名字貌似有點意思了。來看看下面這兩行代碼:

   1: MyDelegate myDelegate = new MyDelegate(this.Test);
   2: myDelegate += new MyDelegate(this.Test1);

通過IL我們可以發現,這里的+=最后就是調用System.Delegate的Combine方法。而Combine的真正實現時在MulticastDelegate的CombineImpl方法中。在MulticastDelegate中有一個_invocationList字段,從CombineImpl中可以看出這個字段是一個object[]類型的,而委托鏈就放在這個數組里。

后記

文章是想到哪兒寫到哪兒,寫的比較亂,也比較匆忙。非常抱歉。對于中間那段奇妙的事情,我原來真的不知道,我一直以為當委托指向一個靜態方法時,_target指向null就完事兒了,沒想到還有這么一番景象。看來很多東西還是不能想當然,親身嘗試一下才知道真實的情況。

1
0
 
標簽:.Net C# 委托
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()