本文是博客園麒麟.NET的《把委託說透》系列的第二篇,重點剖析C#委託的實質。
委託在本質上仍然是一個類,我們用delegate關鍵字聲明的所有委託都繼承自System.MulticastDelegate。後者又是繼承自System.Delegate類,System.Delegate類則繼承自System.Object。委託既然是一個類,那麼它就可以被定義在任何地方,即可以定義在類的內部,也可以定義在類的外部。
正如很多資料上所說的,委託是一種類型安全的函數回調機制, 它不僅能夠調用實例方法,也能調用靜態方法,並且具備按順序執行多個方法的能力。
C#委託揭祕
在把委託說透(1)中可以看到,委託的使用其實是很簡單的。儘管如此,其內部實現仍然相當複雜。.NET強大的編譯器和CLR掩蓋了這種複雜性。
爲了解釋方便,我們把(1)中的委託代碼複製在下面,並做一處小小的改動,將LogToTextFile設置爲實例方法。
- namespace DelegateSample
- {
- public delegate void Log(string message);
- class UserService
- {
- public Log LogDelegate { get; set; }
- public UserService() { }
- public void Register(User user)
- {
- if (user.Name == "Kirin")
- {
- LogDelegate("註冊失敗,已經包含名爲" + user.Name + "的用戶");
- }
- else
- {
- LogDelegate("註冊成功!");
- }
- }
- }
- class Program
- {
- static void Main(string[] args)
- {
- User user = new User { Name = "Kirin", Password = "123" };
- UserService service = new UserService();
- service.LogDelegate = LogToConsole;
- Program p = new Program();
- service.LogDelegate += p.LogToTextFile;
- service.Register(user);
- Console.ReadLine();
- }
- static void LogToConsole(string message)
- {
- Console.WriteLine(message);
- }
- void LogToTextFile(string message)
- {
- using (StreamWriter sw = File.AppendText("log.txt"))
- {
- sw.WriteLine(message);
- sw.Flush();
- sw.Close();
- }
- }
- }
- }
打開Reflector反編譯Log委託,可以看到Log類被編譯爲如下形式:
在上圖中可以得出如下結論:
委託是一個類
可以很清晰的看出Log—>MulticastDelegate—>Delegate這種繼承機制。
儘管委託繼承自System.MulticastDelegate類,但我們並不能顯示地聲明一個繼承自System.MulticastDelegate類的委託。委託必須使用delegate關鍵字聲明,編譯器會自動爲我們生成繼承代碼。
由於委託繼承自System.MulticastDelegate類,自然也繼承MulticastDelegate類的字段、屬性和方法。這些成員中,最重要的當屬三個非公共字段,如下表所示:
字段名稱 | 字段類型 | 描述 |
_target | System.Object | 該字段指明委託所調用的方法所在的實例類型。如果委託調用的爲靜態方法,該字段爲null;如果爲實例方法則爲該方法所在的對象。 |
_methodPtr | System.IntPtr | 標識回調方法的指針。 |
_invocationList | System.Object | 在構建委託鏈時指向一個委託數組,在委託剛剛構建時通常爲null。 |
由上表可以看出,每個委託對象實際上是對方法及其調用時操作的對象的封裝。MulticastDelegate類還定義了兩個只讀公有實例屬性:Target和Method,分別對應_target和_methodPtr。Target屬性返回一個方法回調時操作的對象引用。如果是靜態方法則返回null。Method屬性返回一個標識回調方法的System.Reflection.MethodInfo對象。
編譯器自動爲委託創建了BeginInvoke、EndInvoke和Invoke三個方法
當我們在像調用普通的方法一樣調用委託時,如
- LogDelegate("註冊失敗,已經包含名爲" + user.Name + "的用戶");
這時實際上調用的是編譯器自動生成的Invoke方法
- LogDelegate.Invoke("註冊失敗,已經包含名爲" + user.Name + "的用戶");
使用IL DASM查看UserService的IL代碼,可以驗證以上結論,如下圖所示:
在使用委託時,我們也可以顯示調用Invoke方法(CLR 2.0)。
Invoke方法的參數和返回值與委託是一致的。在調用Invoke方法時,會使用_target和_methodPtr字段。
BeginInvoke和EndInvoke方法用來實現異步調用,本文在此不進行討論。
委託鏈
委託鏈是一個委託的集合,它允許我們調用這個集合中的委託所代表的所有方法(對於有返回值的方法,委託鏈的返回值爲鏈表中最後一個方法的返回值,本文後面會有詳細介紹)。在Delegate類中定義了3個靜態方法來幫助我們操作委託鏈。
- public static Delegate Combine(params Delegate[] delegates);
- public static Delegate Combine(Delegate a, Delegate b);
- public static Delegate Remove(Delegate source, Delegate value);
要理解委託鏈,我們首先基於前面的例子,重新聲明兩個委託:logDel1和logDel2。
- Log logDel1 = LogToConsole;
- Program p = new Program();
- Log logDel2 = p.LogToTextFile;
這兩個委託的_target、_methodPtr和_invocationList值分別如下圖所示:
構造委託鏈
然後,我們使用Combin方法來構造一個委託鏈:
- Log logChain = null;
- logChain = (Log)Delegate.Combine(logChain, logDel1);
由於logChain初始爲null,在使用Combin方法構造委託鏈時,將返回另外一個參數logDel1,再將logDel1的引用賦給logChain。這時logChain將指向logDel1所指向的對象。
接下來我們將logDel2也添加到logChain中來:
- logChain = (Log)Delegate.Combine(logChain, logDel2);
此時,由於logChain已經不再是null,將重新構建一個新的委託對象。該委託對象的_target和_methodPtr字段與logDel2(第二個參數)相同,_invocationList字段將指向一個委託數組。該委託數組中包含兩個元素,第一個元素(索引爲0)指向封裝了LogToConsole方法的委託(即logDel1指向的委託);第二個元素(索引爲1)指向封裝了LogToTextFile方法的委託(即logDel2指向的委託)。最後,將這個新創建的委託對象的引用賦給logChain。
若再將一個新的委託logDel3添加到委託鏈中,則仍然會構建一個新的委託對象,並將logDel3的引用添加到該委託對象_invocationList的末尾(此時鏈表共有3個元素)。然後,再將該委託對象的引用賦給logChain。而logChain之前指向的委託對象則等待垃圾回收。
至此,委託鏈構造完畢,我們來看看如何執行委託鏈表中的委託。由於logChain仍然指向一個委託對象,因此執行委託鏈表的語法與執行委託是一樣的:
- logChain("執行委託鏈");
與普通的委託(如logDel1)所不同的是,logChain的_invocationList字段不爲null。這時將首先遍歷執行_invocationList中的所有委託。所執行的方法的順序與添加的順序一致,依次爲LogToConsole、LogToTextFile。
委託Log的Invoke方法的實現用僞代碼表示如下:
- public void Invoke(string message)
- {
- Delegate[] delegateSet = _InvocationList as Delegate[];
- if (delegateSet != null)
- {
- // 如果委託數組不爲空,則依次執行該委託數組中的委託
- foreach (Feedback d in delegateSet)
- d(value);
- }
- else
- {
- // 如果委託數組爲空,則該委託不代表一個委託鏈
- // 按照正常方式執行該委託
- _methodPtr.Invoke(_target, value);
- }
- }
包含返回值的委託的Invoke實現如下,假設返回值爲string:
- public string Invoke(string message)
- {
- string result = null;
- Delegate[] delegateSet = _InvocationList as Delegate[];
- if (delegateSet != null)
- {
- // 如果委託數組不爲空,則依次執行該委託數組中的委託
- foreach (Feedback d in delegateSet)
- result = d(value);
- }
- else
- {
- // 如果委託數組爲空,則該委託不代表一個委託鏈
- // 按照正常方式執行該委託
- result = _methodPtr.Invoke(_target, value);
- }
- return result;
- }
可以看到在委託鏈中,返回值爲鏈表中最後一個委託的返回值。
那麼如果對兩個委託鏈調用Combine方法呢?
- Log logChain = null;
- Log logChain1 = null;
- Log logChain2 = null;
- logChain1 = (Log)Delegate.Combine(logChain1, logDel1);
- logChain1 = (Log)Delegate.Combine(logChain1, logDel2);
- logChain2 = (Log)Delegate.Combine(logChain2, logDel3;
- logChain2 = (Log)Delegate.Combine(logChain2, logDel4;
- logChain = (Log)Delegate.Combine(logChain1, logChain2);
最終的結果是,logChain的_target和_methodPtr均與logDel4相同(確切地說,兩個委託對象的_methodPtr字段並不相同,但Method屬性是相同的),而_invocationList中委託的順序依次爲logDel1、logDel2、logDel3、logDel4。
綜上所述,可以對Delegate.Combine(Delegate A, Delegate B)方法做如下總結:
1. 如果A和B均爲null,則返回null。
2. 如果A或B一個爲null而另一個不爲null,則返回不爲null的委託。
3. 如果A和B均不爲null,返回一個新的委託,該委託
(1)_target字段與B的_target字段的值相同
(2)Method屬性與B的Method屬性的值相同
(3)_invocationList字段爲一個委託數組,該數組中委託的順序爲:A中_invacationList所指向的委託數組 + B中_invacationList所指向的委託數組。
移除委託鏈
Combine方法用來向委託鏈中添加一個委託,而Remove方法用來從委託鏈中移除一個委託。
logChain = (Log)Delegate.Remove(logChain, new Log(LogToConsole));
當調用Remove時,會遍歷(倒序)第一個參數(logChain)中的中的委託列表(_invocationList字段), 找到與第二個參數(new Log(LogToConsole))的_target和_methodPtr字段相匹配的委託,並將其從委託列表中移除。返回值需分以下幾種情況,爲了描述方便,我們將logChain記爲A,將new Log(LogToConsole)記爲B。
1. 如果A爲null,返回null。
2. 如果B爲null,返回A。
3. 如果A的_invocationList爲null,即不包含委託鏈,那麼如果A本身與B匹配,則返回null,否則返回A。
4. 如果A的_invocationList中不包含與B匹配的委託,則返回A。
5. 如果A的_invocationList中包含與B匹配的委託,則從鏈表中移除B,然後
(1)如果A的鏈表中只剩下一個委託,則返回該委託。
(2)如果A的鏈表中還剩下多個委託,將重新構建一個新的委託R(R的_invocationList字段爲A的_invocationList移除了B之後的鏈表),並返回R。
注意,Remove方法只移除源委託的_invocationList列表中第一個匹配的委託,要想移除所有匹配的委託,可以使用RemoveAll方法。
有了委託鏈,在(1)中提出的第二個疑問就迎刃而解了。當用戶希望使用多種日誌記錄方式的時候,使用委託鏈可以輕鬆地添加和刪除某種日誌記錄方式,從而避免了人爲地維護一個列表。
總結
本文首先介紹了C#委託的實質,委託是一個類,它繼承自System.MulticastDelegate,而MulticastDelegate又繼承自System.Delegate。然後重點剖析了委託鏈,討論瞭如何創建和移除委託鏈。