前言
雖然關於委托的文章園子中不勝枚舉,但是要充分的理解委托的概念並且靈活運用,個人覺得還是要由淺入深,逐步推進,最后再來研究它的實質,這樣才能達到事半功倍的效果,如果不信,請看下文,相信我所言非虛(當然也歡迎園友們拍磚和批評)!
概念
(1)用Delegate類表示委托,委托是一種數據結構,它引用靜態方法或引用類實例及該類的實例方法。
(2)解述:委托聲明了一種類型,它用一種特定的參數以及返回類型來封裝方法。對於靜態方法,委托對象封裝要調用的方法。對於實例方法,委托對象同時封裝一個實例和該實例上的方法。如果有一個委托對象和一組適當的參數,則可以用這組參數來調用委托。
委托引入
平常我們就是寫一個方法,然后再傳入參數直接調用。如下:
static void xsEat(string food) { Console.WriteLine("小三吃" + food); } xsEat("零食"); /*打印出小三吃零食*/
現在我們用委托來實現代碼如下:
delegate void EatDelegate(string food); class Program { static void Main(string[] args) { EatDelegate xs = new EatDelegate(xsEat); xs("零食"); Console.ReadKey(); } static void xsEat(string food) { Console.WriteLine("小三吃" + food); } }
我們用關鍵字 delegate 定義一個EatDelegate委托,在聲明類的任何地方就能聲明委托,委托聲明的返回值和參數必須要和要調用的方法的簽名一致。接下來對委托進行實例化並傳入要代理的方法指針,此時實例化的對象 eat 就指向了方法 xs ,再傳入參數,結果打印出小三吃零食。如果接下來有小紅(xh)和小明(xm)也過來吃零食,那我們也同樣寫這樣的方法,代碼如下:
1 delegate void EatDelegate(string food); 2 class Program 3 { 4 static void Main(string[] args) 5 { 6 EatDelegate xs = new EatDelegate(xsEat); 7 xs("零食"); 8 EatDelegate xh = new EatDelegate(xhEat); 9 xh("零食"); 10 EatDelegate xm = new EatDelegate(xmEat); 11 xm("零食"); 12 Console.ReadKey(); 13 } 14 15 static void xsEat(string food) 16 { 17 Console.WriteLine("小三吃" + food); 18 } 19 20 static void xhEat(string food) 21 { 22 Console.WriteLine("小紅吃" + food); 23 } 24 static void xmEat(string food) 25 { 26 Console.WriteLine("小明吃" + food); 27 } 28 }
上述分別打印出小三吃零食、小紅吃零食、小明吃零食。看起來滿足了需求,但是我們仔細想想明明是吃零食為什么還要實例化三次呢?代碼能不能精簡了,當然有辦法,繼續是委托,這個時候就要用到 委托鏈 了。所以我們對控制台的代碼進行改寫如下:
static void Main(string[] args) { EatDelegate xs = new EatDelegate(xsEat); EatDelegate xh = new EatDelegate(xhEat); EatDelegate xm = new EatDelegate(xmEat); EatDelegate eat; eat = xs + xh + xm; eat("零食"); Console.ReadKey(); }
我們只需要把委托實例添加到委托鏈中即可同樣達到了上述的效果。此時小三、小紅和小明三個一起扎堆吃零食,后來陸陸續續的走了,通過這樣一段描述我們用強大的委托鏈來實現,代碼如下:
1 static void Main(string[] args) 2 { 3 EatDelegate xs = new EatDelegate(xsEat); 4 EatDelegate xh = new EatDelegate(xhEat); 5 EatDelegate xm = new EatDelegate(xmEat); 6 7 EatDelegate eat; 8 Console.WriteLine("小三、小紅和小明一起吃零食"); 9 eat = xs + xh + xm; 10 eat("零食"); 11 Console.WriteLine("小三有約出去了,就剩下小紅和小明吃零食了"); 12 eat -= xs; 13 eat("零食"); 14 Console.WriteLine("小紅也走了,就剩下小明一個人吃零食了"); 15 eat -= xh; 16 eat("零食"); 17 Console.ReadKey(); 18 }
結果運行如圖所示:
我們由此知道-=或者+=號來更容易的對委托鏈中的元素進行操作,這樣一來我們可以隨意而且是任意妄為的對其元素進行操作。對於上面的方法我們繼續進行精簡,由於方法比較簡單,微軟大大為我們提供了一個便捷的方式來實現那就是 匿名方法 。我們將代碼進行改寫如下:
delegate void EatDelegate(string food); class Program { static void Main(string[] args) { EatDelegate eat = null; eat += delegate(string food) { Console.WriteLine("小三吃" + food);}; eat += delegate(string food) { Console.WriteLine("小紅吃" + food); }; eat += delegate(string food) { Console.WriteLine("小明吃" + food); }; eat("零食"); Console.ReadKey(); } }
通過匿名方法使得我們能更加方便的創建委托和使用委托。
通過上述我們不免心生疑問,它只能對靜態方法進行調用,難道不能對動態方法進行調用呢?同時上面的代碼未免有些冗余,也不能體現C#面向對象的思想!所以,鑒於此,我們對上述代碼繼續進行改寫,如下:
1 public class Person 2 { 3 public string Name { get; set; } 4 5 public Person(string name) 6 { 7 this.Name = name; 8 } 9 10 public void Eat(string food) 11 { 12 Console.WriteLine(this.Name + "吃" + food); 13 } 14 } 15 delegate void EatDelegate(string food); 16 class Program 17 { 18 static void Main(string[] args) 19 { 20 21 Person xs = new Person("小三"); 22 Person xh = new Person("小紅"); 23 Person xm = new Person("小明"); 24 EatDelegate xsEat = new EatDelegate(xs.Eat); 25 EatDelegate xhEat = new EatDelegate(xh.Eat); 26 EatDelegate xmEat = new EatDelegate(xm.Eat); 27 EatDelegate eatChain = null; 28 Console.WriteLine("小三、小紅和小明一起吃零食"); 29 eatChain = xsEat + xhEat + xmEat; 30 eatChain("零食"); 31 Console.WriteLine("小三有約,出去剩下小紅和小明吃零食"); 32 eatChain -= xsEat; 33 eatChain("零食"); 34 Console.WriteLine("小紅也走了,只剩下小明一個人吃零食"); 35 eatChain -= xhEat; 36 eatChain("零食"); 37 Console.ReadKey(); 38 } 39 }
結果和之前的效果一樣:
接下來我繼續進行深入的改進,看到這里相信你也明白,方法是可以作為參數進行傳遞的,那么委托作為代理對象是不是可以作為方法的參數進行傳遞呢??我們試試,對上面代碼繼續進行改造,因為三個人吃零食是不確定的,所以會用到不確定參數數組,以及要吃的食物參數,所以改造如下:
1 public class Person 2 { 3 public string Name { get; set; } 4 5 public Person(string name) 6 { 7 this.Name = name; 8 } 9 10 public void Eat(string food) 11 { 12 Console.WriteLine(this.Name + "吃" + food); 13 } 14 } 15 delegate void EatDelegate(string food); 16 class Program 17 { 18 static void Main(string[] args) 19 { 20 21 Person xs = new Person("小三"); 22 Person xh = new Person("小紅"); 23 Person xm = new Person("小明"); 24 EatDelegate xsEat = new EatDelegate(xs.Eat); 25 EatDelegate xhEat = new EatDelegate(xh.Eat); 26 EatDelegate xmEat = new EatDelegate(xm.Eat); 27 EatDelegate eatChain = null; 28 Console.WriteLine("小三、小紅和小明一起吃零食"); 29 EatSomething("零食", xsEat, xhEat, xmEat); 30 Console.WriteLine("小三有約,出去剩下小紅和小明吃零食"); 31 EatSomething("零食", xhEat, xmEat); 32 Console.WriteLine("小紅也走了,只剩下小明一個人吃零食"); 33 EatSomething("零食", xmEat); 34 EatSomething(null, null); 35 Console.ReadKey(); 36 } 37 38 static void EatSomething(string food, params EatDelegate[] ed) 39 { 40 EatDelegate eatChain = null; 41 if (ed == null) 42 { 43 Console.WriteLine("都走了,沒人吃零食"); 44 } 45 else 46 { 47 foreach (var chain in ed) 48 { 49 eatChain += chain; 50 } 51 eatChain(food); 52 Console.WriteLine(); 53 } 54 } 55 }
結果打印出:
上述我們就實現了將委托作為參數進行傳遞並進行動態的調用 !
至此,零食也吃完了,想必你對委托有了一定的了解了,那么難道你沒有疑問?委托這么強大,它到底是什么東西?委托鏈又是怎樣實現的呢?請看下文
委托實質
我們運用反編譯工具查看上述生成的應用程序的IL代碼,如圖:
由圖中我們得出的信息是:(1)我們聲明的委托 EatDelegate 原來是個類,而且還是可不可繼承的密封類。(2)該委托還繼承多播委托 MulticastDelegate ,並且該多播委托最終繼承於 Delegate ,接下來我們看看這個委托:
我們注意到在這委托里面有個IntPtr,后面接着的變量 _methodPtr 這個就是方法指針,我們說到委托鏈就是存的方法指針,那有很多方法它是怎么將這么多方法指針添加進去的呢?之前看過園友老趙中一篇文章,通過IL代碼可以直接看c#代碼,只是把你編寫的C#代碼進行了再一次編譯而已於是我查看我寫的 EatSomething 方法如圖:
方法中添加委托鏈這一段 eatChain += chain; 被編譯成如圖,於是我點擊查看委托中的 Combine 方法,進去后又看到一個方法如圖:
若是在一個委托中添加另外一個委托,先是調用Delegate中的Combine方法,若檢測第一個委托不為空則調用CombineImpl方法,將新添加的委托b添加進去,此時我查看CombineImpl方法,后面緊着就是出錯拋異常,而且是個虛方法,確定應該是被重寫了,既然聲明的委托繼承於多播委托,多播委托繼承委托(Delegate),肯定是在這兩個委托類中,終於在多播委托 MulticastDelegate 中重寫了這個方法,如圖:
接着查看其方法得到如下
通過這幅圖和上幅圖看到,多播委托中 _invovationList object對象在CombineImpl中被轉換成了數組,同時將其num初始化為1,並將多播委托中的要添加的方法指針數量 _invocationCount 賦給num,一直往下,當其添加的數量不夠時,此時將 _invovationList 重新創建數組,數組長度再賦值,有點類似StringBuilder!所以委托鏈的整個過程就是:新添加的委托方法將新創建一個委托對象,並將其方法指針存入最終的父類的變量Intptr中,同時將創建的對象添加到委托數組中去。
Invoke
聲明委托后查看其IL代碼都有三個方法,最主要的是Invoke方法,如圖:
咦,發現里面怎么有個參數food同時和聲明委托方法簽名一致。於是試試將聲明委托的返回值改為有返回值的,此時編譯生成再來查看果然是一樣的。也就是說當你實例化委托對象所指的方法時,此時同樣可以用實例化委托對象的 Invoke 指向該方法,也就是說調用委托其實就是調用委托中的Invoke方法,並遍歷委托里面的數組,依次調用里面的方法。
總結
(1)聲明委托的本質是聲明一個類,並且該聲明委托extends=>MulticastDelegate=>Delegate
(2)委托鏈的本質就是新添加的方法將創建一個新的委托對象,並將其方法指針存入最終父類Delegate中的變量Intptr中,與此同時將新創建的對象添加到委托的對象數組中去
(3)調用委托的本質是調用實例化委托對象中的Invoke方法,遍歷其委托對象數組,依次調用數組中的方法