參考:
框架設計(第二版):CLR Via C#——15.4 委托揭秘(P281)
正文:
代碼1-1,這是一個簡單的委托使用。
using System;
using System.Collections.Generic;
using System.Text;
namespace Delegate
{
public class DelegateTest
{
protected delegate void MyDelegate();
private void TestMethod() { }
private void Method()
{
MyDelegate aMyDelegate = new MyDelegate(TestMethod);
Method(aMyDelegate);
}
private void Method(MyDelegate aMyDelegate)
{
if (aMyDelegate != null)
{
aMyDelegate();
}
}
}
}
代碼1-1
從表面上看,委托似乎很容易使用:用C#的delegate關鍵字來定義,用我們都熟悉的new操作符來構造委托實例,用我們熟悉的方法調用語法來調用回調函數(不過要用引用了委托對象的變量來代替方法名)。
然而,實際情況比前面幾個例子所演示的復雜得多。編譯器和CLR做了大量的幕後工作來隱藏復雜性。本節將集中講解編譯器和CLR是如何實現委托的。掌握這些知識有助於我們理解委托,並學會如何更好地使用它們。與此同時,本節還要適當地介紹委托的其它一些特征。
首先重新查看下面這行代碼:代碼1-2
public delegate void MyDelegate();
代碼1-2
看到這行代碼時,編譯器實際上會像下面這樣定義一個完整的類:代碼1-3
public class MyDelegate : System.MulticastDelegate
{
//構造器
public MyDelegate(Object aobject,IntPtr method);
//方法的原型與源代碼指定的相同
public virtual void Invoke();
//允許異步回調的方法
public virtual IAsyncResult BeginInvoke(
AsyncCallback callback, object aobject);
public virtual void EndInvoke(IAsyncResult result);
}
代碼1-3
編譯器定義的類有4個方法:一個構造器、Invoke、BeginInvoke和EndInvoke。在本章,要重點解釋構造器和Invoke方法。
事實上,可以驗證編譯器確實會自動生成這個類,具體做法就是用ILDasm.exe來查看生成的程序集(assembly),如圖1-1所示。
圖1-1 ILDasm.exe 顯示了編譯器為委托生成的元數據
在這個例子中,編譯器定義了一個類,名為MyDelegate,該類繼承自 Framework Class Library(FCL)中定義的System.MulticastDelegate 類型(所有委托類型都繼承自 MulticastDelegate)。
重要提示: System.MulticastDelegate 類繼承自 system.Delegate,後者本身繼承自System.Object。之所以有兩個委托類,是有歷史原因的,同時也是很遺憾的,FCL中本應只有一個委托類。沒有辦法,我們需要了解這兩個類,因為即使構建的所有委托類型都把MulticastDelegate作為基類,我們仍然會在個別情況下用Delegate類(而不是MulticastDelegate類)定義的方法來處理自己構建的這些委托類型。例如,Delegate 類有兩個靜態方法,分別名為 Combine 和 Remove 。這兩個方法的前面指出它們要取 Delegate 參數。因為委托類型繼承自 MulticastDelegate,後者又繼承自 Delegate,所以委托類型的實例可以被傳入這二個方法。
委托類有Public可見性,因為委托在源代碼中被聲明為Public類。我們應該知道,委托類可以在一個類型內部(即嵌套在另一個類型內)或在全局范圍內定義。簡單地說,因為委托是類,在可以定義類的任何地方,都可以定義委托。
因為所有委托類型都繼承 MulticastDelegate,所以它們都繼承了 MulticastDelegate 的字段、屬性和方法。在所有這些成員中,有3個非公共字段是最重要的。
字段:_target
類型:System.Object
描述:當委托對象封裝一個靜態方法時,這個字段為 null。當委托對象封裝一個實例方法時,這個字段引用的是調用回調方法是要操作的對象。換而言之,這個字段指名要傳給實例方法的隱式 this 參數的值。
字段:_methodPtr
類型:System.IntPtr
描述:一個內部的整數值,CLR用它標識要回調的方法。
字段:_invocationList
類型:System.Object
描述:該字段通常為null。在構建一個委托鏈時,它可以引用一個委托數組。
注意,所有委托都有一個構造器,該函數取2個參數:一個是對象引用,另一個是引用回調方法的一個整數。然而,如果仔細查看代碼1-1的源代碼,會發現傳遞的是 TestMethod。根據我們所學的編程知識判斷,這段代碼不會通過編譯!
然而,C#編譯器知道正在構建的是委托,所以會解析源代碼以確定引用的是哪個對象和方法。對象引用被傳遞給構造器的aobject參數,一個特殊的標識方法的 IntPtr值(從MethodDef 或 MemberRef 元數據標記獲得)被傳遞給method 參數。對於靜態方法,null 被傳遞給 object 參數。在構造器內部,這兩個參數分別保存在 _target 和 _methodPtr 私有字段內。
除此之外,構造器還將 _invocationList 字段設置為 null。
如此看來,每個委托對象實際封裝了一個方法和調用該方法時要操作的一個對象。
知道了委托對象如何構造,並了解了其內部結構之後,我們要談談回調方法是如何調用的。 為了方便起見,下面重復了代碼1-1中的 Method(MyDelegate aMyDelegate)方法的代碼:
private void Method(MyDelegate aMyDelegate)
{
if (aMyDelegate != null)
{
aMyDelegate();
}
}
if 語句首先檢查 aMyDelegate 是否為 null。如果 aMyDelegate 不為 null,你會看到下一行代碼調用了回調方法。null 檢查必不可少,因為 aMyDelegate 實際上只是一個可能引用 MyDelegate 委托對象的變量,它也可能為 null。這段代碼看上去就像是調用了一個名為 aMyDelegate 的函數。但是,這裡沒有名為 aMyDelegate 的函數。再次重申,因為編譯器知道 aMyDelegate 是一個引用了一個委托對象的變量,所以會生成代碼調用該委托對象的Invoke方法。換而言之,編譯器看到以下代碼時:
aMyDelegate();
將生成一下代碼,好像源代碼本來就是這麼寫的一樣:
aMyDelegate.Invoke();
為了驗證編譯器生成代碼來調用委托的 Invoke 方法,我們可以用 ILDasm.exe 來檢查為 void Method(MyDelegate aMyDelegate) 方法創建的IL代碼。圖1-2 展示了 void Method(MyDelegate aMyDelegate) 方法的中間語言(intermediate language,IL)代碼。
圖1-2 ILDasm.exe 證明編譯器生成了對 MyDelegate 委托類型的 Invoke 方法的調用
事實上,我們可以修改 void Method(MyDelegate aMyDelegate) 方法來顯示調用 Invoke,如下所以:
private void Method(MyDelegate aMyDelegate)
{
if (aMyDelegate != null)
{
aMyDelegate.Invoke();
}
}
我們還記得編譯器在定義 MyDelegate 類的時候定義 Invoke 的。所以在調用 Invoke 的時候,它使用的是 private _target 和 _methodPtr 字段來對指定對象調用所需的方法。注意,Invoke 方法的簽名匹配委托的簽名,因為 MyDelegate 委托是一個無參數並返回類型為 void,Invoke 方法(如編譯器生成的那樣)也是一個無參數並返回 void。
End.