第二部分 设计类型:第11章 事件
事件是实现交互的类型成员。
定义事件需要提供以下能力:
1.方法可登记/注销它对该事件的关注。
2.改事件发生时,登记了的方法会收到通知。
类型之所以能提供事件通知功能,是因为类型维护了一个已登记方法的列表。事件发生后,类型将通知列表中所有已登记的方法。
为了理解事件在CLR中的工作机制,举个实用的场景:
假定要设计一个电子邮件程序。电子邮件到达时,用户希望将该邮件转发给传真机或寻呼机。
构建这个应用程序时,假定先设计一个名为MailManager的类型,负责接收传入的电子邮件。MailManager类型公开了一个名为NewMail的事件。其他类型(如Fax和Pager)的对象可登记它们对这个事件的关注。MailManager收到信邮件时,会引发该事件,将邮件分发给每一个登记的对象。
应用程序初始化时,只实例化一个MailManager实例。然后,应用程序可实例化任意数量的Fax和Pager对象。
11.1 设计要公开事件的类型
11.1.1 第1步:定义类型来容纳所有需要发送给事件通知接收者的附加信息
事件对象想向接收通知的对象传递一些附加信息,那么需要封装到自己的类中,包含私有字段和只读公共属性。根据约定,这种类型应该从System.EventArgs类派生,并且类名以EventArgs结束。
//首先 定义类型来容纳发送给接受者的附加信息 internal class NewMailEventArgs:EventArgs{ //附加信息的私有字段 private readnoly string m_from,m_to,m_subject; //附加信息的只读属性 public string From{ get{return m_from;}} public string To{ get{return m_to;}} public string subject { get{return m_subject;}} public NewMailEventArgs(string from,string to,string subject){ m_from = from; m_to = to; m_subject = subject; } } //后续的步骤在MailManager类中进行 internal class MailManager{ }
11.1.2 第2步:定义事件成员
事件成员使用event关键字来定义。
每个事件成员要指定一下内容:
1.一个可访问性标示符(public)。
2.一个委托类型,它指出要调用的方法的原型。
3.一个名称(可以是任意有效的标示符)。
MailManager类的事件成员:
internal class MailManager{ //第2步:定义事件成员 public event EventHandler<NewMailEventArgs> NewMail; ... }
NewMail是这个事件的名称。
事件成员的类型是EventHandler<NewMailEventAgrs>,意味着“事件通知”的所有接受者都必须提供一个原型和EventHandler<NewMailEventArgs>委托类型匹配的回调方法。
由于泛型System.EventHandler委托类型的定义如下:
public delegate void EventHandler<TEventArgs>(Object sender,TEventArgs e) where TEventArgs: EventArgs;
所以方法原型必须具有以下形式:
void MethodName(Object sender,NewMailEventArgs e);
11.1.3 第3步:定义负责引发事件的方法来通知事件的登记对象
根据约定,类应该定义一个受保护的虚方法。
要引发事件时,当前类及其派生类中的代码会调用该方法。该方法要获取一个参数,也就是NewMailEventArgs对象(包含给接收对象的信息)。该方法默认实现只检查是否有对象登记了对事件的关注。有就引发事件通知登记对象。
该方法在MailManager类中看起来像这样:
internal class MailManager{ ... //第3步:定义一个负责引发事件的方法,它通知已登记的对象 //事件已经发生。如果类似密封的,这个方法要声明为私有和非虚 protected virtual void OnNewMail(NewMailEventArgs e){ //出于线程安全的考虑,现在将对委托字段的引用复制到一个临时字段中 EventHandler<EventArgs> temp = Interlocked.CompareExchange(ref NewMail,null,null); //任何方法登记了对事件的关注,就通知它们 if(temp!=null) temp(this,e); } ... }
为了方便起见,可定义一个扩展方法来封装这个线程安全逻辑。如下:
public static class EventArgExtensions{ public static void Raise<TEventArgs>(this TEventArgs e, Object sender,ref EventHandler<TEventArgs> eventDelegate) where TEventArgs: EventArgs{ //出于线程安全考虑,现在将对委托字段的引用复制到一个临时字段中 EventHandler<TEventArgs> temp = Interlocked.CompareExchage(ref eventDelegate,null,null); //任何方法登记了对我们的事件的关注,就通知它们 if(temp!=null) temp(sender,e); } }
现在,可以像下面这样重新编写OnNewMail方法:
protected virtual void OnNewMail(NewMailEventArgs e){ e.Raise(this,ref m_NewMail); }
使用MailManager作为基类可自由重新OnNewMail方法。这个能力使派生类能控制事件的引发,从而以自己的方式处理新邮件。一般情况下,派生类会调用基类的OnNewMail方法,使登记的方法能收到通知。然而,派生类也可以不允许事件的转发。
11.1.4 第4步:定义方法将输入转化为期望事件
类还需要有一个方法来获取一些输入,并把它转化为事件的引发。
在MailManager的例子中,是调用SimulateNewMail方法来指出一封新的电子邮件已到达MailManager:
internal class MailManager{
//第4步:定义方法,将输入转化为期望事件
public void SimulateNewMail(string from,string to,string subject){
//构造一个对象来容纳想传给通知接受者的信息
NewMailEventArgs e = new NewMailEventArgs(from,to,subject);
//调用虚方法通知对象事件已发生
//如果没有类型重新该方法,我们的对象将通知事件的所有登记对象
OnNewMail(e);
}
}
SimulateNewMail接收一些关于邮件的信息,并构造一个NewMailEventArgs对象,将邮件信息传给它的构造器。然后调用MailManager自己的虚方法OnNewMail来正式通知MailManager对象收到了新的电子邮件。这通常会导致事件的引发,从而通知所有登记的方法。(如前所属,MailManager的派生类可能重写这个行为。)
11.2 编译器如何实现事件
MailManger类中,定义了事件成员本身:
public event EventHandler<NewMailEventArgs> NewMail;
C#编译时会转换为3个构造:
1.一个被初始化为null的私有委托字段
2.一个公告add_Xxx方法(Xxx是事件名)
3.一个公告remove_Xxx方法(Xxx是事件名)
11.3 设计侦听事件的类型
定义类型来使用另一个类型提供的事件。
Fax类型代码:
internal sealed class Fax{ public Fax(MailManger mm){ //构造EventHandler<NewMailEventArgs>委托的一个实例, //使它引用我们的FaxMsg回调方法, //向MailManager的NewMail事件登记我们的回调方法 mm.NewMail+=FaxMsg; } //新邮件到达时,MailManager将调用这个方法 private void FaxMsg(Object sender,NewMailEventArgs e){ //'sender'表示MailManager对象,便于将信息传回给它 //'e'表示MailManager对象想传给我们的附件事件信息 //这里的代码正常情况下应该是传真邮件 //但这个测试性的实现只是在控制台上显示邮件 Console.WriteLine("Faxing mail message:"); Console.WriteLine(" From={0},To={1},Subject={2}"),e.From,e.To,e.Subject); } //执行完传真方法后,Fax对象将向NewMail事件注销自己对它的关注, //以后不再接收通知 public void Unregister(MailManager mm){ //向MailManager的NewMail事件注销自己对这个事件的关注 mm.NewMail -= FaxMsg; } }
C#编译+=操作符为:
mm.add_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg));
如果你的类型要实现IDisposable的Dispose方法,就应该在实现中注销对所有事件的关注。
C#编译-=操作符为:
mm.remove_NewMail(new EventHandler<NewMailEventArgs>(FaxMsg));
11.4 显式实现事件
System.Windows.Forms.Control类型定义了约70个事件。
大部分时候程序员只用到少数几个事件,所以从Control派生类创建的对象都浪费大量内存。
事件内部的实现:
首先实现的是EventSet类,它代表一个集合,其中包含了事件及每个事件的委托列表。