本文參考自:https://wenku.baidu.com/view/41ab91d3c1c708a1284a44d7.html?qq-pf-to=pcqq.c2c
1、為什么委托定義的返回值通常為void?
盡管並非必須,但是大多數情況委托定義的返回值都為void,因為這部分委托基本都是需要綁定多個方法,也就是當前委托允許多個訂閱者注冊,但是當主函數執行委托對象上注冊的方法時,不會返回結果,只會返回最后一個方法的結果值,這一點可以通過調試下面的代碼就可以看出,代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Delegate { public delegate int? OpearteEventHandler(int? a,int? b); class Program { static void Main(string[] args) { Program p = new Program(); int? a = 10; int? b = 6; OpearteEventHandler ah = p.Add; ah += p.Sub; ah += p.Multiply; p.ShowResult(a, b, ah); } public void ShowResult(int? a, int? b, OpearteEventHandler handler) { string result = ""; result += handler(a, b).ToString(); Console.WriteLine(result); Console.ReadKey(); } public int? Add(int? a, int? b) { return a + b; } public int? Sub(int? a, int? b) { return a - b; } public int? Multiply(int? a, int? b) { return a * b; } } }
對上面的代碼進行調試發現,Add方法和Sub方法的結果值並沒有被返回,只返回了最后Multiply的值,除了上面這個原因外,發布者和訂閱者的關系是松耦合的,發布者根本不關心誰訂閱了它的事件,為什么要訂閱,跟別說返回值了,發布者要做的就是執行訂閱它事件的方法,所以當委托綁定了多個事件時,返回值常常是void的原因.
2、如何讓事件只允許一個客戶訂閱
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Delegate { public delegate string GeneralEventHandler(); class Program { static void Main(string[] args) { Publisher pr = new Publisher(); pr.Register(new Program().ReturnValue); pr.DoSomething(); } public string ReturnValue() { return "返回值"; } public string ReturnValue1() { return "返回值1"; } public class Publisher { private event GeneralEventHandler NumberChanged;//聲明一個事件 public void Register(GeneralEventHandler handler) { NumberChanged += handler; } public void UnRegister(GeneralEventHandler handler) { NumberChanged -= handler; } public void DoSomething() { if (NumberChanged != null) { string str = NumberChanged(); Console.WriteLine("return value is {0}", str); } } } } }
注意:
(1)、在UnRegister()中,沒有進行任何判斷就使用了NumberChanged -= method 語句。這是因為即使method 方法沒有進行過注冊,此行語句也不會有任何問題,不會拋出異常,僅僅是不會產生任何效果而已。
(2)、NumberChanged被聲明為私有的,所以客戶端無法看到它,所以無法通過它來觸發事件,調用訂閱者的方法,而只能通過Register()和UnRegister()方法來注冊和取消注冊
但是上面的代碼並不是最好的實現,C#提供事件訪問器,也可以實現上面的功能
3、事件訪問器
C#提供事件訪問器,通過它可以將委托封裝成一個變量,像訪問類中的屬性那樣,來訪問事件,代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Delegate { public delegate string GeneralEventHandler(); class Program { static void Main(string[] args) { Publisher pr = new Publisher(); pr.NumberChanged += new Subscriber().ReturnValue; pr.DoSomethings(); } public class Publisher { private GeneralEventHandler numberChanged;//聲明一個委托變量 // 事件訪問器的定義 public event GeneralEventHandler NumberChanged { add { numberChanged = value; } remove { numberChanged -= value; } } public void DoSomethings() { if (numberChanged != null) { string str = numberChanged(); Console.WriteLine("return value is {0}", str); } } } public class Subscriber { public string ReturnValue() { return "返回值1"; } } } }
上面代碼中的類似屬性的public event GeneralEventHandler NumberChanged{ add{....}remove{....} }就是事件訪問器了,使用了事件訪問器之后,DoSomethings就只能通過numberChanged委托變量來觸發事件了,而不能使用NumberChanged訪問器來觸發,因為它只用於注冊和取消注冊事件。
4、獲得多個返回值與異常處理
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Delegates { public delegate string GeneralEventHandler(int num); class Program { static void Main(string[] args) { Publisher pr = new Publisher(); pr.NumberChanged +=new GeneralEventHandler(new Subscriber().ReturnValue); pr.NumberChanged += new GeneralEventHandler(new Subscriber1().ReturnValue); List<string> list=pr.DoSomething(); Console.WriteLine(list[0]+"..."+list[1]); } public class Publisher { public event GeneralEventHandler NumberChanged;//定義一個事件 public List<string> DoSomething() { List<string> list = new List<string>(); if (NumberChanged == null) return list; //獲得委托數組 Delegate[] delArr = NumberChanged.GetInvocationList(); foreach (Delegate del in delArr) { GeneralEventHandler handler = (GeneralEventHandler)del;//拆箱 list.Add(handler(100)); } return list; } } public class Subscriber { public string ReturnValue(int num) { Console.WriteLine("Subscriber1 invoked, number:{0}", num); return "[Subscriber returned]"; } } public class Subscriber1 { public string ReturnValue(int num) { Console.WriteLine("Subscriber1 invoked, number:{0}", num); return "[Subscriber1 returned]"; } } } }
上面的方法獲得了兩個訂閱者的返回值,但是前面說過很多情況下,委托的定義都不包含返回值,所以上面的方法介紹的似乎沒什么實際意義。但是其實上面這種方法來觸發事件的情況應該是在異常處理中,因為很有可能在觸發事件時,訂閱者的方法拋出異常,這一異常可能會引起發布者的異常,使得發布者的程序停止,而后面的訂閱者的方法將不會被執行,所以我們需要加上異常處理。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Delegates { public delegate void GeneralEventHandler(object sender, EventArgs e); class Program { static void Main(string[] args) { Publisher pr = new Publisher(); pr.NumberChanged += new GeneralEventHandler(new Subscriber().OnEvent); pr.NumberChanged += new GeneralEventHandler(new Subscriber1().OnEvent); pr.NumberChanged += new GeneralEventHandler(new Subscriber2().OnEvent); pr.DoSomethings(); } public class Publisher { public event GeneralEventHandler NumberChanged;//定義一個事件 public void DoSomethings() { Program.TraverseEvent(NumberChanged, this, EventArgs.Empty); } } public class Subscriber { public void OnEvent(object sender, EventArgs e) { Console.WriteLine("Subscriber Invoked!"); } } public class Subscriber1 { public void OnEvent(object sender, EventArgs e) { throw new Exception("Subscriber1 Failed"); } } public class Subscriber2 { public void OnEvent(object sender, EventArgs e) { Console.WriteLine("Subscriber2 Invoked!"); } } /// <summary> /// 遍歷所有的訂閱事件,獲得多個返回值以及異常處理 /// </summary> /// <param name="del">綁定完方法的委托</param> /// <param name="args">傳遞給訂閱方法的參數</param> /// <returns></returns> public static object[] TraverseEvent(Delegate del, params object[] args) { List<object> list = new List<object>(); if (del != null) { Delegate[] delArr = del.GetInvocationList();//獲得委托鏈表 foreach (Delegate method in delArr) { try { object obj = method.DynamicInvoke(args);//執行訂閱方法,並傳遞參數,獲得其返回值 if (obj != null) { list.Add(obj); } } catch{ } } } return list.ToArray(); } } }
DynamicInvoke方法是調用委托最通用的方法了,適用於所有類型的委托。它接受的參數為object[],也就是說它可以將任意數量的任意類型作為參數,並返回單個object 對象。
ok,通過結果發現Subscriber1().OnEvent訂閱方法拋出的異常並沒有影響Subscriber2().OnEvent的方法的執行。當然因為
catch什么都沒有做!
5、訂閱者方法超時的處理
訂閱者除了可以通過異常的方式影響發布者外,還可以通過另外一種方式影響發布者:超時,一般說超時指的是方法的執行超過了某個時間,而這里的含義是,方法的執行的時間比較長,2s、3s、5s都算做超時,是一個很模糊的概念。而超時和異常的區別就在於,超時並不會影響事件的正確觸發和正常的運行,卻會導致事件觸發后需要很長時間才會結束,在依次執行訂閱者方法的這段時間內,客戶端程序會被中斷,什么也不能做。應為當執行訂閱者中的方法時(通過委托相當於依次調用了所有注冊了的方法),當前線程會轉到訂閱者的方法中,調用訂閱者方法的客戶端則會被中斷,只有當方法執行完畢並返回時,控制權才會重新回到調用訂閱者方法的客戶端的客戶端中。如果你調試過上面案例的代碼的話,我相信這個特點不難發現。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Delegates { public class Program { static void Main(string[] args) { Publisher pr = new Publisher(); pr.MyEvent += new EventHandler(new Subscriber().OnEvent); pr.MyEvent += new EventHandler(new Subscriber1().OnEvent); pr.MyEvent += new EventHandler(new Subscriber2().OnEvent); pr.DoSomethings(); } public static object[] traverseEvent(Delegate del,params object[] args) { List<object> list = new List<object>(); if (del != null) { Delegate[] dels=del.GetInvocationList(); foreach (Delegate method in dels) { try { //執行傳入委托的訂閱方法,傳入參數,並且獲得其返回值,而且如果傳入委托中間部分的訂閱事件發生異常,不會影響后面事件的執行 object obj = method.DynamicInvoke(args); if (obj != null) { list.Add(obj); } } catch{ } } } return list.ToArray(); } } public class Publisher { public event EventHandler MyEvent; public void DoSomethings() { Console.WriteLine("DoSomethings invoked!"); Program.traverseEvent(MyEvent, this, EventArgs.Empty); } } public class Subscriber { public void OnEvent(object sender,EventArgs args) { throw new Exception("Subscriber Failed"); } } public class Subscriber1 { public void OnEvent(object sender, EventArgs args) { Console.WriteLine("Subscriber1 begin execute,time is {0}", DateTime.Now.ToLongTimeString()); Thread.Sleep(3000); Console.WriteLine("Wait for 3 seconds,Subscriber1 executed end, time is {0}", DateTime.Now.ToLongTimeString()); } } public class Subscriber2 { public void OnEvent(object sender, EventArgs args) { Console.WriteLine("Subscriber2 begin execute,time is {0}",DateTime.Now.ToLongTimeString()); Console.ReadKey(); } } }
通過方法的執行事件可以發現,Subscriber2方法是在Subscriber1的方法等待3秒之后才執行的,但是在前面說過,很多情況下,尤其是在遠程調用的時候(比如所在Remoting中),發布者和訂閱者應該是完全的松耦合的,發布者不關心誰訂閱了它,為什么要訂閱它,訂閱它的方法有什么返回值,不關心訂閱者方法會不會拋出異常,當然也不關心訂閱者方法需要多少時間才能執行完畢.它只要在事件的發生的一剎那告訴訂閱者事件已經發生,並將相關參數傳遞給訂閱者事件。而訂閱者方法不管是執行失敗還是超時都不應該影響發布者,而上面的例子發布者必須等待Subscriber1中的發發執行完畢才會執行Subscriber2中的方法,所以需要解決這個問題。
我們都知道委托實際上是一種數據結構,當每定義一個委托,實際上這個委托實例都會繼承自MulticastDelegate這個完整的類,而MulticastDelegate這個類則會繼承Delegate數據結構,而MulticastDelegate類中包含Invoke()和BeginInvoke()和EndInvoke()等方法,所以間接的每個委托的實例也可以調用這些方法。下面是一個委托被調用的過程:
(1)、調用Invoke方法,中斷發布者客戶端的操作
(2)、開啟一個線程
(3)、通過線程去執行所有訂閱者的方法
(4)、所有訂閱者方法執行完畢,將控制權返還給發布者客戶端
注意:Invoke()方法是同步執行的,也就是說如果某一個訂閱方法超時了,那么其下面的方法會等到它執行完畢之后,在執行
ok,介紹完Invoke之后,想必上面的超時問題為什么會發生,應該一目了然了,結下了開始講解決方法,BeginInvoke()和EndInvoke()方法,在.NET中異步執行的方法通常會成對出現,並且以Begin和End作為方法的開頭(如Stream 類的BeginRead()和EndRead()方法了),他們用於方法的異步執行.
(1)、BeginInvoke()方法簡介:即在發布者客戶端吊用委托之后,當前委托實例調用BeginInvoke()方法,該方法是異步執行,它會從線程池中抓取一個閑置線程,交由這個線程去執行訂閱者中的方法,而客戶端線程則繼續執行接下來的代碼,通過這種多線程的方式,達到了異步的效果,也避免了上面單線程阻塞的問題。
(2)、BeginInvoke()方法接受"動態"的參數個數和類型,具體的參數個數是根據調用BeginInvoke方法的委托所決定的,代碼如下:
public delegate void EventHandler1(string a,int b); eh.BeginInvoke("a", 1, null, null);
這里的代碼可能不合理,但只是舉例說明,這里調用BeginInvoke()方法的是EventHandler,EventHandler委托接受兩個參數string和int,所以BeginInvoke前兩個參數也是string和int,這個是編譯時,根據委托的定義動態生成的.
(3)、BeginInvoke()方法接受"動態"的參數個數和類型,但最后兩個參數是確定的,一個是AsyncCallback(回調函數),另一個是object
(4)、當在委托上調用BeginInvoke方法時,當委托對象只能包含一個方法,對於有多個訂閱者注冊的情況,只能通過GetInvocationList()獲取委托鏈表,遍歷它們,分別操作
(5)、如果訂閱者方法拋出異常,.NET會捕捉到它,但是只有在調用EndInvoke()方法時,才會將異常拋出,在本例中,因為我們不關心訂閱者的情況,所以無需處理異常,因為即使異常拋出,也是在執行訂閱者方法的線程上,所以不會影響到發布者客戶端,客戶端甚至不知道訂閱者發生了異常,這有時是好事有時是壞事.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Delegates { public delegate void GeneralEventHandler(object sender, EventArgs e); class Program { static void Main(string[] args) { Publisher pr = new Publisher(); pr.NumberChanged += new EventHandler(new Subscriber().OnEvent); pr.NumberChanged += new EventHandler(new Subscriber1().OnEvent); pr.NumberChanged += new EventHandler(new Subscriber2().OnEvent); pr.DoSomethings(); } public class Publisher { public event EventHandler NumberChanged;//定義一個事件 public void DoSomethings() { if (NumberChanged != null) { Delegate[] delArr = NumberChanged.GetInvocationList();//獲得委托鏈表 foreach (Delegate method in delArr) { EventHandler handler = (EventHandler)method; handler.BeginInvoke(this, EventArgs.Empty, null, null); } } } } public class Subscriber { public void OnEvent(object sender, EventArgs e) { Thread.Sleep(TimeSpan.FromSeconds(3));//模擬休息3秒 Console.WriteLine("Wait for 3 seconds,Subscriber Invoked!"); } } public class Subscriber1 { public void OnEvent(object sender, EventArgs e) { throw new Exception("Subscriber1 Failed"); //模擬拋出異常 } } public class Subscriber2 { public void OnEvent(object sender, EventArgs e) { Console.WriteLine("Subscriber2 Invoked!"); } } } }
ok,通過結果發現,Subscriber2的方法最先執行,並沒有等待Subscriber的方法執行完畢,而且Subscriber1的異常也沒有拋出,發布者客戶端並沒有因為這個異常而停止操作。
6、委托和方法的異步調用
通常情況下,如果需要異步執行一個耗時的操作,我們會新開一個線程,然后讓這個線程去執行代碼。但是對於每一個異步調用都用線程去操作顯然會對性能造成影響,同時操作也相對繁瑣一些,.NET中可以通過委托進行方法的異步調用,就是說客戶端在異步調用方法時,本身並不會因為方法的調用而終止,而是從線程中抓取一個線程去執行該方法,主線程繼續執行自己的代碼,這樣就實現了代碼的並行執行,使用線程池的好處就是避免了頻繁的進行異步調用時,創建、銷毀線程的開銷。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Delegates { public delegate int AddEventHandler(int a,int b); class Program { static void Main(string[] args) { Console.WriteLine("Client Application Started!The time is {0}",DateTime.Now); Thread.CurrentThread.Name = "主線程"; Calculator cal = new Calculator(); AddEventHandler handler = new AddEventHandler(cal.Add); IAsyncResult asyncResult = handler.BeginInvoke(6, 6, null, null); for (int i = 1; i <= 3; i++) { Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("{0}: 主線程休息了 {1} 秒.", Thread.CurrentThread.Name, 1); } int res = handler.EndInvoke(asyncResult); Console.WriteLine("異步調用Add方法的結果: {0}\n", res); Console.WriteLine("Client Application ended!The time is {0}", DateTime.Now); Console.WriteLine("按任意鍵繼續..."); Console.ReadLine(); } } } public class Calculator { public int Add(int a, int b) { if (Thread.CurrentThread.IsThreadPoolThread) { Thread.CurrentThread.Name = "線程池中的線程"; } Console.WriteLine("-----------------------------------------------"); Console.WriteLine("Add方法開始執行!"); for (int i = 0; i <= 2; i++) { Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("{0}: 休息了 {1} 秒.", Thread.CurrentThread.Name, 1); } Console.WriteLine("Add方法執行完畢!"); Console.WriteLine("-----------------------------------------------"); return a + b; } }
從輸出可以看出,整個應用程序執行了3秒種時間,但是主線程和子線程一共休息了6秒,所以可以推斷出,主線程和子線程是並行的,不是串行的EndInvoke方法獲得了返回值.
接下來說BeginInvoke方法的另外兩個參數,一個是AsyncCallback是一個委托類型,它用於方法的回調,也就是當異步方法調用完畢時,自動調用的方法,它的定義為:
public delegate void AsyncCallback(IAsyncResult ar);
第二個參數是Object類型用於傳遞任何你想要的數據,它可以通過IAsyncResult的AsyncState屬性獲得
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Remoting.Messaging; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Delegates { public delegate int AddEventHandler(int a,int b); class Program { static void Main(string[] args) { Console.WriteLine("Client Application Started!The time is {0}",DateTime.Now); Thread.CurrentThread.Name = "主線程"; Calculator cal = new Calculator(); AddEventHandler handler = new AddEventHandler(cal.Add); AsyncCallback callBack = new AsyncCallback(OnAddComplete); int data = 666; IAsyncResult asyncResult = handler.BeginInvoke(6, 6, callBack, data); for (int i = 1; i <= 3; i++) { Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("{0}: 主線程休息了 {1} 秒.", Thread.CurrentThread.Name, 1); } Console.WriteLine("Client Application ended!The time is {0}", DateTime.Now); Console.WriteLine("按任意鍵繼續..."); Console.ReadLine(); } static void OnAddComplete(IAsyncResult asyncResult) { AsyncResult result = (AsyncResult)asyncResult; AddEventHandler del = (AddEventHandler)result.AsyncDelegate; int data = (int)asyncResult.AsyncState; int rtn = del.EndInvoke(asyncResult); Console.WriteLine("{0}: Result, {1}; Data: {2}\n", Thread.CurrentThread.Name, rtn, data); } } } public class Calculator { public int Add(int a, int b) { if (Thread.CurrentThread.IsThreadPoolThread) { Thread.CurrentThread.Name = "線程池中的線程"; } Console.WriteLine("-----------------------------------------------"); Console.WriteLine("Add方法開始執行!"); for (int i = 0; i <= 2; i++) { Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("{0}: 休息了 {1} 秒.", Thread.CurrentThread.Name, 1); } Console.WriteLine("Add方法執行完畢!"); Console.WriteLine("-----------------------------------------------"); return a + b; } }
ok,異步方法執行完畢之后,立即調用了OnAddComplete方法,並且data數據成功傳遞了過去;
注意:
(1)、在調用EndInvoke方法時可能會拋出異常,所以需要加到try{}catch{}塊中
(2)、執行回調方法的線程並不是Main Thread,而是Pool Thread
(3)、我們在調用BeginInvoke()后不再需要保存IAysncResult 了,因為AysncCallback 委托將該對象定義在了回調方法的參數列表中
(4)、通過BeginInvoke()最后一個Object參數,可以給回調函數傳參