首页 > 代码库 > C#系列文章之事件
C#系列文章之事件
文件涉及的内容:
- 设计公开事件类型
- 编译器如何实现事件
- 设计侦听事件的类型
- 显式实现事件
事件:定义了事件成员的类型允许类型通知其他对象发生特定的事情。
CLR事件模型以委托为基础,委托是调用回调方法的一种类型安全的方式,对象凭借调用方法接收他们订阅的通知。
定义了事件成员的类型要求能够提供以下功能:
- 方法能登记它对事件的关注
- 方法能注销它对事件的关注
- 事件发生时,登记的方法将收到通知
本文章以一个电子邮件应用程序为例。当电子邮件到达时,用户希望将邮件转发给传真机或寻呼机进行处理。先设计MainlManager类型来接收传入的电子邮件,它公开NewMain事件。其他类型(Fax或Pager)对象登记对于该事件的关注。MailManager收到新电子邮件会引发该事件,造成邮件分发给每个已登记的对象,它们都有自己的方式处理邮件。
1.1设计要公开事件的类型
第一步:定义类型来容纳所有需要发送给事件通知接收者的附加信息
该类型通常包含一组私有字段以及一些用于公开这些字段的只读公共属性。
1 class NewMailEventArgs:EventArgs 2 { 3 private readonly string m_from, m_to, m_subject; 4 public NewMailEventArgs(string from,string to,string subject) 5 { 6 m_from = from; 7 m_to = to; 8 m_subject = subject; 9 } 10 public string From { get { return m_from; } } 11 public string To { get{ return m_to; } } 12 public string Subject { get { return m_subject; } } 13 }
第二步:定义事件成员
class MailManager { public event EventHandler<NewMailEventArgs> NewMail; }
其中NewMail是事件名称。事件成员类型是EventHandler<NewMailEventArgs>说明事件通知的所有接收者都必须提供一个原型和其委托类型匹配的回调方法。由于泛型System.EventHandler委托类型的定义如下:
public delegate void EventHandler<TEventArgs>(Object sender,TEventArgs e);
所以方法原型必须具有以下形式:void MethodName(Object sender,NewMailEventArgs e);之所以事件模式要求所有事件处理程序的返回类型都是void,是因为引发事件后可能要调用好几个回调方法,但没办法获得所有方法的返回值,返回void就不允许回调方法有返回值。
第三步:定义负责引发事件的方法来通知事件的登记对象
1 /// <summary> 2 /// 定义负责引发事件的方法来通知事件的登记对象,该方法定义在MailManager中 3 /// 如果类是密封的,该方法要声明为私有和非虚 4 /// </summary> 5 /// <param name="e"></param> 6 protected virtual void OnNewMail(NewMailEventArgs e) 7 { 8 //出于线程安全考虑,现在将委托字段的引用复制到一个临时变量中 9 EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail); 10 if(temp!=null) 11 { 12 temp(this, e); 13 } 14 }
上面方法使用了Volatile.Read()方法确保线程安全,主要考虑下面两种情况:
1.直接判断NewMail!=null,但在调用NewMail之前,另一个线程可能从委托链中移除了一个委托,使其为空,从而发生(NullReferenceException)异常。
2.有些人可能也会将其保存在一个临时变量中,但未使用Volatile,理论上可以但是如果编译器发生优化代码移除该临时变量,那就和第一种情况一样。
使用Volatile.Read会强迫NewMail在这个调用发生时读取,引用必须复制到temp变量中,比较完美的解决方式。但是在单线程的中不会出现这种情况
第四步 定义方法将输入转化为期望事件
1 public void SimulateNewMail(string from,string to,string subject) 2 { 3 //构造一个对象来容纳想传给通知接收者的信息 4 NewMailEventArgs e = new NewMailEventArgs(from, to, subject); 5 //调用虚方法通知对象事件已反生 6 //如果没有类型重写该方法 7 //我们的对象将通知事件的所有登记对象 8 OnNewMail(e); 9 }
该方法指出一封新的邮件已到达MailManager。
1.2 编译器如何实现事件
在MailManager类中我们用一句话定义事件成员本身:public event EventHandler<NewMailEventArgs> NewMail;
C#编译器会转换为以下代码:
//一个被初始化为null的私有字段 private EventHandler<NewMailEventArgs> NewMail = null; public void add_NewMail(EventHandler<NewMailEventArgs> value) { //通过循环和对CompareExchange的调用,以一种线程安全的方式向事件添加委托 //CompareExchange是把目标操作数(第1参数所指向的内存中的数) //与一个值(第3参数)比较,如果相等, //则用另一个值(第2参数)与目标操作数(第1参数所指向的内存中的数)交换 EventHandler<NewMailEventArgs> prevHandler; EventHandler<NewMailEventArgs> newMail = this.NewMail; do { prevHandler = newMail; EventHandler<NewMailEventArgs> newHandler = (EventHandler<NewMailEventArgs>)Delegate.Combine(prevHandler, value); newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, newHandler, prevHandler); } while (newMail != prevHandler); } public void remove_NewMail(EventHandler<NewMailEventArgs> value) { EventHandler<NewMailEventArgs> prevHandler; EventHandler<NewMailEventArgs> newMail = this.NewMail; do { prevHandler = newMail; EventHandler<NewMailEventArgs> newHandler = (EventHandler<NewMailEventArgs>)Delegate.Remove(prevHandler, value); newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, newHandler, prevHandler); } while (newMail != prevHandler); }
本实例中,add和remove方法可访问性都是public是因为事件NewMail声明为public,事件的可访问性决定了什么代码能登记和注销对事件的关注。但无论如何只有类型本身才能访问上述委托字段NewMail。除了上述生成的代码,编译器还会在托管程序集的元数据中生成事件定义记录项。包含一些标志和基础委托类型。CLR本身并不使用这些元数据信息运行时只需要访问器方法。
1.3 设计侦听事件的类型
如何定义一个类型来使用另一个类型提供的事件。以Fax类型为例:
internal class Fax { public Fax(MailManager mm) { //向MailManager的NewMail事件登记我们的回调方法 mm.NewMail += FaxMsg; } //新邮件到达,MailManager将调用这个方法 //sender表示MailManager对象,便于将信息回传给它 //e表示MailManager对象想传给我们的附加事件信息 private void FaxMsg(object sender, NewMailEventArgs e) { Console.WriteLine("Fax 的消息from:{0} to:{1} subject:{2}", e.From, e.To, e.Subject); } /// <summary> /// 注销 /// </summary> /// <param name="mm"></param> public void Unregister(MailManager mm) { mm.NewMail -= FaxMsg; } }
电子邮件应用程序初始化时首先构造MailManager对象,并将对该对象的引用保存到变量中。然后构造Fax对象,并将MailManager对象引用作为实参传递。在Fax构造器中,使用+=登记对NewMail事件的关注。
1.4 显式实现事件
对于System.Windows.Forms.Control类型定义了大约70个事件。每个从Control派生类型创建对象都要浪费大量内存,而大多数我们只关心少数几个事件。如何通过显式实现事件来高效的实现提供了大量事件的类思路如下:
定义事件时:公开事件的每个对象都要维护一个集合(如字典)。集合将某种事件标识符作为健,将委托列表作为值。新对象构造时集合也是空白。登记对一个事件的关注会在集合中查找事件的标识符。如果事件标识符存在,新委托就和这个事件的委托列表合并,否则就添加事件标识符和委托。
引发事件时:对象引发事件会在集合中查找事件的标识符,如果没有说明没有对象登记对这个事件的关注,所以也没委托需要回调。否则就调用与它关联的委托列表。
1 public sealed class EventKey { } 2 public sealed class EventSet 3 { 4 //定义私有字典 5 private readonly Dictionary<EventKey, Delegate> m_events = 6 new Dictionary<EventKey, Delegate>(); 7 /// <summary> 8 /// 不存在添加,存在则和现有EventKey合并 9 /// </summary> 10 public void Add(EventKey eventKey,Delegate handler) 11 { 12 //确保操作唯一 13 Monitor.Enter(m_events); 14 Delegate d; 15 //根据健获取值 16 m_events.TryGetValue(eventKey, out d); 17 //添加或合并 18 m_events[eventKey] = Delegate.Combine(d, handler); 19 Monitor.Exit(m_events); 20 } 21 /// <summary> 22 /// 删除委托,在删除最后一个委托时还需删除字典中EventKey->Delegate 23 /// </summary> 24 public void Remove(EventKey eventKey,Delegate handler) 25 { 26 Monitor.Enter(m_events); 27 Delegate d; 28 //TryGetValue确保在尝试从集合中删除不存在的EventKey时不会抛出异常 29 if (m_events.TryGetValue(eventKey,out d)) 30 { 31 d = Delegate.Remove(d, handler); 32 if(d!=null) 33 { 34 //如果还有委托,就设置新的头部 35 m_events[eventKey] = d; 36 } 37 else 38 { 39 m_events.Remove(eventKey); 40 } 41 } 42 Monitor.Exit(m_events); 43 } 44 /// <summary> 45 /// 为指定的EventKey引发事件 46 /// </summary> 47 public void Raise(EventKey eventKey,Object sender,EventArgs e) 48 { 49 Delegate d; 50 Monitor.Enter(m_events); 51 m_events.TryGetValue(eventKey, out d); 52 Monitor.Exit(m_events); 53 if(d!=null) 54 { 55 //利用DynamicInvoke,会向调用的回调方法查证参数的类型安全, 56 //并调用方法,如果存在类型不匹配,就抛异常 57 d.DynamicInvoke(new Object[] { sender, e }); 58 } 59 } 60 }
接下来定义类来使用EventSet
1 public class FooEventArgs : EventArgs { } 2 public class TypeWithLotsOfEvents 3 { 4 //用于管理一组"事件/委托" 5 private readonly EventSet m_eventSet = new EventSet(); 6 //受保护的属性使派生类型能访问集合 7 protected EventSet EventSet { get { return m_eventSet; } } 8 //构造一个静态只读对象来标识这个事件 9 //每个对象都有自己的哈希码,以便在对象的集合中查找这个事件的委托链表 10 protected static readonly EventKey s_fooEventKey = new EventKey(); 11 //定义事件访问器方法,用于在集合中增删委托 12 public event EventHandler<FooEventArgs> Foo 13 { 14 add { m_eventSet.Add(s_fooEventKey, value); } 15 remove { m_eventSet.Remove(s_fooEventKey, value); } 16 } 17 //为这个事件定义受保护的虚方法 18 protected virtual void OnFoo(FooEventArgs e) 19 { 20 m_eventSet.Raise(s_fooEventKey, this, e); 21 } 22 //定义将输入转换成这个事件的方法 23 public void SimulateFoo() { OnFoo(new FooEventArgs()); } 24 }
如何使用TypeWithLotsOfEvent,只需按照标准的语法向事件登记即可
1 static void Main(string[] args) 2 { 3 TypeWithLotsOfEvents twle = new TypeWithLotsOfEvents(); 4 twle.Foo += HandleFooEvent; 5 twle.SimulateFoo(); 6 Console.Read(); 7 } 8 9 private static void HandleFooEvent(object sender, FooEventArgs e) 10 { 11 Console.WriteLine("成功"); 12 }
C#系列文章之事件