步步为营 C# 技术漫谈 五、事件与委托机制
概述
C#中的委托类似于C或C++中的函数指针。使用委托使程序员可以将方法引用封装在委托对象内。然后可以将该委托对象传递给可调用所引用方法的代码,而不必在编译时知道将调用哪个方法。与C或C++中的函数指针不同,委托是面向对象,而且是类型安全的。
C#中的“事件”是当对象发生某些事情时,类向该类的客户提供通知的一种方法。事件最常见的用途是用于图形用户界面;通常,表示界面中的控件的类具有一些事件,当用户对控件进行某些操作(如单击某个按钮)时,将通知这些事件。
使用委托来声明事件。委托对象封装一个方法,以便可以匿名调用该方法。事件是类允许客户为其提供方法(事件发生时应调用这些方法)的委托的一种方法。事件发生时,将调用其客户提供给它的委托。
注明:委托是对方法的包装 在不确定要调用什么方法时候而又不能用抽象或者多态实现的时候用委托。
委托在Observer模式示例:
先创建PilotLamp.cs:
public interface PilotLamp { /// <summary> /// green light /// </summary> void TurnOn(); /// <summary> /// notice /// </summary> string Notice { get; set; } }
再创建DelegateEvent.cs:
public delegate void EventHandler();
再创建TrafficLight.cs:
public class TrafficLight : PilotLamp { public event EventHandler Notices; private string notice; #region GreenLight 成员 public void TurnOn() { if (Notices != null) Notices(); } public string Notice { get { return notice; } set { notice = value; } } #endregion }
再创建Driver.cs:
public class Driver { private string Name; private PilotLamp greenLight; public Driver(string name, PilotLamp greenLight) { this.Name = name; this.greenLight = greenLight; } public void GoLeft() { Console.WriteLine(string.Format("{1}司机,{0},请向左开车.", greenLight.Notice, Name)); } }
再创建Pedestrian.cs:
public class Pedestrian { private string Name; private PilotLamp greenLight; public Pedestrian(string name, PilotLamp greenLight) { this.Name = name; this.greenLight = greenLight; } public void GoThrough() { Console.WriteLine( string.Format("{0}同志,{1},请向前走.", Name, greenLight.Notice)); } }
最后再调用:
public partial class Run : Form { public Run() { InitializeComponent(); } private void btnRun_Click(object sender, EventArgs e) { //------------------------------------- TrafficLight trafficLight = new TrafficLight(); Driver driverOne = new Driver("张三", trafficLight); Driver driverTwo = new Driver("李四", trafficLight); Pedestrian pedestrianOne = new Pedestrian("王五", trafficLight); Pedestrian pedestrianTwo = new Pedestrian("麻六", trafficLight); trafficLight.Notices += new Observer.EventHandler(driverOne.GoLeft); trafficLight.Notices += new Observer.EventHandler(driverTwo.GoLeft); trafficLight.Notices += new Observer.EventHandler(pedestrianOne.GoThrough); trafficLight.Notices += new Observer.EventHandler(pedestrianTwo.GoThrough); trafficLight.Notice = "绿灯亮了."; trafficLight.TurnOn(); //------------------------------------- } }
输出时选控制台应用程序如图:
结果如下图:
事件的使用示例:
namespace DelegateAndEvent { class Program { static void Main(string[] args) { Publishser pub = new Publishser(); OneScriber oneSub = new OneScriber(); TwoScriber twoSub = new TwoScriber(); ThreeScriber threeSub = new ThreeScriber (); pub.NumberChanged += new GeneralEventHandler(oneSub.OnNumberChanged); pub.NumberChanged += new GeneralEventHandler(twoSub.OnNumberChanged); pub.NumberChanged += new GeneralEventHandler(threeSub.OnNumberChanged); pub.DoSomething(); } } public delegate string GeneralEventHandler(); public class Publishser { public event GeneralEventHandler NumberChanged; public void DoSomething() { if (NumberChanged != null) { Delegate[] generalEventHandlers = NumberChanged.GetInvocationList(); foreach (Delegate generalEventHandler in generalEventHandlers) { GeneralEventHandler mothed = (GeneralEventHandler)generalEventHandler; string rtn = mothed(); Console.WriteLine(rtn); System.Threading.Thread.Sleep(2000); } } } } public class OneScriber { public string OnNumberChanged() { return "One Subscriber"; } } public class TwoScriber { public string OnNumberChanged() { return "Two Subscriber"; } } public class ThreeScriber { public string OnNumberChanged() { return "Three Subscriber"; } } }
运行结果:
注意到Delegate是GeneralEventHandler
的基类,所以为了触发事件,先要进行一个向下的强制转换,之后才能在其上触发事件,调用所有注册对象的方法。除了使用这种方式以外,还有一种更灵活方式可以调用方法,它是定义在Delegate基类中的DynamicInvoke()方法:
public object DynamicInvoke(params object[] args);
这可能是调用委托最通用的方法了,适用于所有类型的委托。它接受的参数为object[],也就是说它可以将任意数量的任意类型作为参数,并返回单个object对象。上面的DoSomething()方法也可以改写成下面这种通用形式:
代码作如下改动:
namespace DelegateAndEvent { class Program { static void Main(string[] args) { Publishser pub = new Publishser(); OneScriber oneSub = new OneScriber(); TwoScriber twoSub = new TwoScriber(); ThreeScriber threeSub = new ThreeScriber(); pub.NumberChanged += new GeneralEventHandler(oneSub.OnNumberChanged); pub.NumberChanged += new GeneralEventHandler(twoSub.OnNumberChanged); pub.NumberChanged += new GeneralEventHandler(threeSub.OnNumberChanged); List<string> strlist = pub.DoSomething(); foreach (string result in strlist) Console.WriteLine(result); System.Threading.Thread.Sleep(5000); } } public delegate string GeneralEventHandler(); public class Publishser { public event GeneralEventHandler NumberChanged; public List<string> DoSomething() { List<string> strList = new List<string>(); if (NumberChanged == null) return strList; Delegate[] generalEventHandlers = NumberChanged.GetInvocationList(); foreach (Delegate generalEventHandler in generalEventHandlers) { // GeneralEventHandler mothed = (GeneralEventHandler)generalEventHandler; string rtn = generalEventHandler.DynamicInvoke(null).ToString(); strList.Add(rtn); } return strList; } } public class OneScriber { public string OnNumberChanged() { return "One Subscriber"; } } public class TwoScriber { public string OnNumberChanged() { return "Two Subscriber"; } } public class ThreeScriber { public string OnNumberChanged() { return "Three Subscriber"; } } }
结果如下:
还是一样的结果.
委托的定义会生成继承自MulticastDelegate的完整的类,其中包含Invoke()、BeginInvoke()和EndInvoke()方法。当我们直接调用委托时,实际上是调用了Invoke()方法,它会中断调用它的客户端,然后在客户端线程上执行所有订阅者的方法(客户端无法继续执行后面代码),最后将控制权返回客户端。注意到BeginInvoke()、EndInvoke()方法,在.Net中,异步执行的方法通常都会配对出现,并且以Begin和End作为方法的开头(最常见的可能就是Stream类的BeginRead()和EndRead()方法了)。它们用于方法的异步执行,即是在调用BeginInvoke()之后,客户端从线程池中抓取一个闲置线程,然后交由这个线程去执行订阅者的方法,而客户端线程则可以继续执行下面的代码。
BeginInvoke()接受“动态”的参数个数和类型,为什么说“动态”的呢?因为它的参数是在编译时根据委托的定义动态生成的,其中前面参数的个数和类型与委托定义中接受的参数个数和类型相同,最后两个参数分别是AsyncCallback和Object类型,对于它们更具体的内容,可以参见下一节委托和方法的异步调用部分。现在,我们仅需要对这两个参数传入null就可以了。另外还需要注意几点:
- 在委托类型上调用BeginInvoke()时,此委托对象只能包含一个目标方法,所以对于多个订阅者注册的情况,必须使用GetInvocationList()获得所有委托对象,然后遍历它们,分别在其上调用BeginInvoke()方法。如果直接在委托上调用BeginInvoke(),会抛出异常,提示“委托只能包含一个目标方法”。
- 如果订阅者的方法抛出异常,.NET会捕捉到它,但是只有在调用EndInvoke()的时候,才会将异常重新抛出。而在本例中,我们不使用EndInvoke()(因为我们不关心订阅者的执行情况),所以我们无需处理异常,因为即使抛出异常,也是在另一个线程上,不会影响到客户端线程(客户端甚至不知道订阅者发生了异常,这有时是好事有时是坏事)。
- BeginInvoke()方法属于委托定义所生成的类,它既不属于MulticastDelegate也不属于Delegate基类, 我们需要进行一个向下转换,来获取到实际的委托类型。
示例:
namespace DelegateAndEvent { class Program { static void Main(string[] args) { Publishser pub = new Publishser(); OneScriber oneSub = new OneScriber(); TwoScriber twoSub = new TwoScriber(); ThreeScriber threeSub = new ThreeScriber(); pub.NumberChanged += new GeneralEventHandler(oneSub.OnNumberChanged); pub.NumberChanged += new GeneralEventHandler(twoSub.OnNumberChanged); pub.NumberChanged += new GeneralEventHandler(threeSub.OnNumberChanged); List<string> strlist = pub.DoSomething(); foreach (string result in strlist) Console.WriteLine(result); System.Threading.Thread.Sleep(5000); } } public delegate string GeneralEventHandler(object sender,EventArgs e); public class Publishser { public event GeneralEventHandler NumberChanged; public List<string> DoSomething() { List<string> strList = new List<string>(); if (NumberChanged == null) return strList; Delegate[] generalEventHandlers = NumberChanged.GetInvocationList(); foreach (Delegate generalEventHandler in generalEventHandlers) { GeneralEventHandler mothed = (GeneralEventHandler)generalEventHandler; IAsyncResult result = mothed.BeginInvoke(this, EventArgs.Empty, null, null); string str = mothed.EndInvoke(result); strList.Add(str); } return strList; } } public class OneScriber { public string OnNumberChanged(object sender,EventArgs e) { return "One Subscriber"; } } public class TwoScriber { public string OnNumberChanged(object sender, EventArgs e) { return "Two Subscriber"; } } public class ThreeScriber { public string OnNumberChanged(object sender, EventArgs e) { return "Three Subscriber"; } } }
结果:
BeginInvoke的另外两个参数分别是AsyncCallback和Object类型,其中AsyncCallback是一个委托类型,它用于方法的回调,即是说当异步方法执行完毕时自动进行调用的方法。它的定义为:
public delegate void AsyncCallback(IAsyncResult ar);
Object类型用于传递任何你想要的数值,它可以通过IAsyncResult的AsyncState属性获得。
步步为营 C# 技术漫谈系列
步步为营 C# 技术漫谈 一、反射机制
步步为营 C# 技术漫谈 二、ASP.NET 页生命周期
步步为营 C# 技术漫谈 三、公共语言运行库(CLR)
步步为营 C# 技术漫谈 四、垃圾回收机制(GC)
作者:spring yang
出处:http://www.cnblogs.com/springyangwc/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。