引言:
上一專題介紹了下編譯器是如何來翻譯委託的,從中繼語言的角度去看委託,希望可以協助大家進一步的理解委託,然而之前的介紹都是委託只是封裝一個方法,那委託能不能封裝多個方法呢?因為生活中經常會聽到,我代表大家的意見等這樣的說話,既然委託也是一個代表,那他如果只能代表一個人,那他的魅力就不是很大了吧,所以我們就會委託能不能代表多個方法的? 答案是可以的,這就是本專題要講的內容——委託鏈,委託鏈也是一個委託,只是因為它是把多個委託鏈在一起,所以我們就以委託鏈來這麼稱呼它的。
一、到底什麼是委託鏈
我們平常執行個體化委派物件時都是綁定一個方法的, 前一個專題介紹的委託也是封裝了一個方法的, 用前面的例子就是委派律師的只有一個人,也就是當事人只有一個的,但是現實生活中顯然不是這樣的,在官司的時候律師可以同時接多個案子,也是接收多個當時人的委派,這樣,該律師就與多個當事人綁定在一起了, 需要瞭解多個當事人的案例情況的。其實這就是生活中的委託鏈,此時這位律師不僅僅是一個人的代表律師了,而是多個當事人的律師。生活中的委託鏈和C#中的委託鏈很類似的,現在就說說C#中的委託鏈到底是個什麼的?
首先委託鏈就是一個委託,所以大家不要看到委託鏈感覺又是什麼C#中的新特性的,然而要把多個委託鏈在一起,就必須儲存多個委託的引用,那委託鏈對象是在哪裡儲存多個委託的引用的呢?還記得我們上一專題中,我們介紹的委託類型有三個非公用欄位的嗎?這三個欄位是——_target,methodPtr 和_invocationList,至於這三個欄位具體代表什麼大家可以查看我的上一專題的文章,然而_invocationList 欄位正是儲存多個委託引用的地方的。
為了更好的解釋_invocationList是如何來儲存委託引用的,下面先看一個委託鏈的例子和運行結果,然後再分析原因:
using System;namespace DelegateTest{ public class Program { // 聲明一個委託類型,它的執行個體引用一個方法 // 該方法回去一個int 參數,返回void類型 public delegate void DelegateTest(int parm); public static void Main(string[] args) { // 用靜態方法來執行個體化委託 DelegateTest dtstatic = new DelegateTest(Program.method1); // 用執行個體方法來執行個體化委託 DelegateTest dtinstance = new DelegateTest(new Program().method2); // 隱式調用委託 dtstatic(1); // 顯式調用Invoke方法來調用委託 dtinstance.Invoke(1); // 隱式調用委託 dtstatic(2); // 顯式調用Invoke方法來調用委託 dtinstance.Invoke(2); Console.Read(); } private static void method1(int parm) { Console.WriteLine("調用的是靜態方法,參數值為:" + parm); } private void method2(int parm) { Console.WriteLine("調用的是執行個體方法,參數值為:" + parm); } }}
運行結果:
下面就來分析下為什麼會出現這樣的結果的:
一開始我們執行個體化了兩個委託變數,如下代碼:
// 用靜態方法來執行個體化委託 DelegateTest dtstatic = new DelegateTest(Program.method1); // 用執行個體方法來執行個體化委託 DelegateTest dtinstance = new DelegateTest(new Program().method2);
委託變數dtstatic和dtinstance引用的委派物件的初始狀態如:
然後我們定義了一個委託類型的引用變數delegatechain,剛開始它沒有任何委派物件,是一個Null 參考,當我們執行下面的一行代碼時,
delegatechain = (DelegateTest)Delegate.Combine(delegatechain, dtstatic);
Combine方法發現試圖合并的是null和dtstatic,在內部,Combine直接返回dtstatic中的對象,此時delegatechain和dtstatic變數引用的都是同一個委派物件,如所示:
為了示範委託鏈,我們通過代碼在再添加一個委託,此時就再調用了Combine方法,代碼如下:
delegatechain = (DelegateTest)Delegate.Combine(delegatechain, dtinstance);
這時候,Combine方法發現delegatechain已經引用了一個委派物件了(此時已經引用了destatic引用的委派物件了),所以Combine會構造一個新的委派物件(這一點很想String.Concat,我們簡單的使用是通過+操作符把兩個字串串連起來,關於字串的討論大家可以參考我部落格中的這篇文章http://www.cnblogs.com/zhili/archive/2012/06/25/String_StringBuilder.html),這個新的委派物件會對它的私人欄位_target 和_methodPtr欄位進行初始化,然後此時_invocationList欄位初始化為引用了一個委派物件的數組,這個數組的第一個元素(下標為0)就是被初始化為引用封裝了method1方法的委託,數組的二個元素被初始化為引用封裝了method2方法的委託(也就是dtinstance引用的委派物件),最後delegaechain被設為引用建立的這個委派物件,下面是一個圖,可以協助大家理解委託鏈(也叫多播委託):
同樣的道理,如果是添加第三個委託給委託鏈,過程也是和上面一樣的, 此時又會建立一個委派物件,此時_invocationList欄位會初始化為引用一個儲存這三個委派物件數組,然而有人會問了——對於已經引用了委派物件的委託類型變數調用Combine方法後會建立一個新的委派物件,然後對新的這個委派物件的三個欄位進行重新初始化話,最後把之前的委託類型變數引用新建立的委派物件(這裡就幫大家總結下委託鏈的建立過程),那之前的委派物件怎麼辦呢? 相信大部分人會有這個疑問的,這點和字串的Concat方法很像,之前的委派物件和——invocationList欄位引用的數組會被記憶體回收掉(正是因為這樣,委託和字串String一樣是不可變的)。
注意:我們還可以調用Delegate的Remove方法從鏈中刪除委託,如調用下面代碼時:
delegatechain =(DelegateTest)Delegate.Remove(delegatechain,new DelegateTest(method1));
Remove方法被調用時,它會掃描delegateChain(第一個參數)所引用的委派物件內部維護的委託數組(如果對於委託數組為空白的情況下調用Remove方法將不會有任何作用,就是不會刪除任何委託引用,這裡主要是說明掃描是從委託數組裡進行掃描),如果找到delegateChain引用的委派物件的_target和_methodPtr欄位
和第二個參數(新建立的委託)中的欄位匹配的委託,如果刪除之後數組中只剩下一個資料項目時,就返回那個資料項目(而不會去建立一個委派物件再初始化的,此時的_invocationList為null,而不是儲存一個委派物件引用的數組了,具體可以Remove一個後調試看看的),如果此時數組中還剩餘多個資料項目,就建立一個委派物件——其中建立並初始化_invocationList數組(此時的數組引用的委派物件已經少了一個了,因為用Remove方法刪除了),並且,每次Remove方法調用只能從鏈中刪除一個委託,而不會刪除有匹配的_target和_methodPtr欄位的所有委託(這個大家可以調試看看的)
二、如何對委託鏈中的委託調用進行控制
通過上面相信大家可以理解如何建立一個委託鏈對象的,但是從運行結果中還可以看出,每次調用委託鏈時,委託鏈封裝的每個方法都會順序被執行,如果委託鏈中被調用的委託拋出一個異常,這樣鏈中的後續所有對象都不能被調用,並且如果委託的前面具有一個非void的傳回型別,則只有最後一個傳回值會被保留,其他所有回調方法的傳回值都會被捨棄,這就意味著其他所有操作的傳回值都永遠看不到的嗎? 事實卻不是這樣的,我們可以通過調用Delegate.GetInvocationList方法來顯式調用鏈中的每一個委託,同時可以添加一些自己的定義輸出。
GetInvocationList方法返回一個由Delegate引用構成的數組,其中每一個數組都指向鏈中的一個委派物件。在內部,GetInvocationList建立並初始化一個數組,讓資料的每一個元素都引用鏈中的一個委託,然後返回對該數組的一個引用。如果_invocatinList欄位為null,返回的數組只有一個元素,該元素就是委託執行個體本身。下面就通過一個程式來示範下的:
namespace DelegateChainDemo{ class Program { // 聲明一個委託類型,它的執行個體引用一個方法 // 該方法回去一個int 參數,返回void類型 public delegate string DelegateTest(); static void Main(string[] args) { // 用靜態方法來執行個體化委託 DelegateTest dtstatic = new DelegateTest(Program.method1); // 用執行個體方法來執行個體化委託 DelegateTest dtinstance = new DelegateTest(new Program().method2); DelegateTest dtinstance2 = new DelegateTest(new Program().method3); // 定義一個委託鏈對象,一開始初始化為null,就是不代表任何方法(我就是我,我不代表任何人) DelegateTest delegatechain = null; delegatechain += dtstatic; delegatechain += dtinstance; delegatechain += dtinstance2; ////delegatechain =(DelegateTest)Delegate.Remove(delegatechain,new DelegateTest(method1)); ////delegatechain = (DelegateTest)Delegate.Remove(delegatechain, new DelegateTest(new Program().method2)); Console.WriteLine(Test(delegatechain)); Console.Read(); } private static string method1() { return "這是靜態方法1"; } private string method2() { throw new Exception("拋出了一個異常"); } private string method3() { return "這是執行個體方法3"; } // 測試調用委託的方法 private static string Test(DelegateTest chain) { if (chain == null) { return null; } // 用這個變數來儲存輸出的字串 StringBuilder returnstring = new StringBuilder(); // 擷取一個委託數組,其中每個元素都引用鏈中的委託 Delegate[] delegatearray=chain.GetInvocationList(); // 遍曆數組中的每個委託 foreach (DelegateTest t in delegatearray) { try { //調用委託獲得傳回值 returnstring.Append(t() + Environment.NewLine); } catch (Exception e) { returnstring.AppendFormat("異常從 {0} 方法中拋出, 異常資訊為:{1}{2}", t.Method.Name, e.Message, Environment.NewLine); } } // 把結果返回給調用者 return returnstring.ToString(); } }}
運行結果:
從運行結果可以看出,此時我們可以獲得每一個回調方法的傳回值,並且可以加入一些自訂的傳回值的(程式中加入了換行字串),這樣就可以對委託鏈中的每個委派物件進行控制了,即使其中一個拋出異常,此時我們也可以進行捕獲,而不會導致後續的委派物件不能被調用的問題。
三、總結下
本專題主要介紹如何建立一個委託鏈以及對於建立一個委託鏈的過程進行了詳細的分享,第二部分主要先指出了委託了一些局限性,然後通過調用GetInvocationList方法來返回一個委託數組,這樣就可以通過遍曆委託數組中的每個委託來通知委託的調用過程,這樣就可以對委託鏈的調用進行更多的控制的。到此本專題也就介紹完了,通過這三個專題對委託的介紹,相信大家會對委託有一個更深的理解,然後為什麼要寫三個專題來詳細介紹委託的呢? 主要是後面要介紹的事件,Lambda運算式,Linq方面的內容都是和委託有關係的,所以更好的理解委託將是後面特性的一個基礎,希望這些對大家有協助,我將在下一個專題裡面介紹事件。
在這裡希望大家多多支援下我的IT部落格大賽的,我的參賽首頁是:http://blog.51cto.com/contest2012/6146675,希望大家幫忙投個票的,謝謝大家的支援