本文参考自: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参数,可以给回调函数传参