第11章 事件
11.1 发布事件
{
// 1、在MailManager内部定义MailMsgEventArgs类型
public class MailMsgEventArgs : EventArgs
{
// 传递给事件接受者的类型定义信息
public MainMsgEventArgs(String from, String to, String subject, String body)
{
this.from = from;
this.to = to;
this.subject = subject;
this.body = body;
}
public readonly String from, to, subject, body;
}
// 2、下面的委托类型定义了接受者必须实现的回调方法原型
public delegate void MainMsgEventHandler(Object sender, MainMsgEventArgs args);
// 3、事件成员
public event MainMsgEventHandler MailMsg;
// 4、下面的受保护虚方法负责通知事件的登记对象
protected virtual void OnMailMsg(MailMsgEventArgs e)
{
// 有对象登记事件吗?
if(MailMsg != null)
{
// 如果有,则通知委托链表上的所有对象
MailMsg(this, e);
}
}
// 5、下面的方法将输入转化为期望的事件,该方法在新的电子邮件消息达到时被调用
public void SimulateArrivingMsg(String from, String to, String subject, String body)
{
// 构造一个对象保存希望传递给通知接受者的信息
MailMsgEventArgs e = new MailMsgEventArgs(from, to, subject, body);
// 调用虚方法通知对象事件已发生,如果派生类型没有重写该虚方法,对象将通知所有登记的事件侦听者
OnMailMsg(e);
}
}
设计MailManager类型的开发人员必须定义以下几项:
1、定义一个类型用于保存所有需要发送给事件通知接受者的附加信息
按照.NET框架的约定,所有保存事件信息的类型都应该继承自System.EventArgs,且名称应该以EventArgs结束。EventArgs类型在FCL中的定义为:
public class EventArgs
{
public static readonly EventArgs Empty = new EventArgs();
public EventArgs() { }
}
当我们定义一个不需要传递任何额外数据的事件时可以直接使用EventArgs.Empty而不用再构造新的EventArgs对象
2、定义一个委托类型,用于指定事件触发时被调用的方法原型
按.NET框架的约定,委托类型的名称应该以EventHandler结束,并且回调方法的原型应该有一个void返回值并接受两个参数(FCL中的某些事件处理器如System.ResolveEventHandler违反了该约定)。如果我们定义的事件没有需要传递给事件接受者的附加信息,我们便不必定义新的委托类型,而可以直接使用FCL中的System.EventHandler,并将EventArgs.Empty传给第二个参数。Eventhandler原型如下:
3、定义一个事件成员
4、定义一个受保护的虚方法,负责通知事件的登记对象
5、定义一个方法,将输入转化为期望的事件
定义MailMsg事件时,C#编译器会把代码翻译成以下3个构造:
private MailMsgEventHandler MailMsg = null;
// 2、一个允许对象登记事件的“公有” add_* 方法
[MethodImplAttribute(MethodImplOptions.Synchronized)]
public virtual void add_MailMsg(MailMsgEventHandler handler)
{
MailMsg = (MailMsgEventHandler)Delegate.Combine(MailMsg, handler);
}
// 3、一个允许对象注销事件的“公有” remove_* 方法
[MethodImplAttribute(MethodImplOptions.Synchronized)]
public virtual void remove_MailMsg(MailMsgEventHandler handler)
{
MailMsg = (MailMsgEventHandler)Delegate.Remove(MailMsg, handler);
}
生成的委托字段MailMsg总是为私有字段,这样可以防止类型外的代码错误的对该字段进行了操作。C#编译器自动在事件名称前添加add_和remove_来命名登记和注销事件的方法,这两个方法上都应用了一个MethodImplAttribute特性,使这些方法被标识为同步方法,实现了线程安全
除了上述3个构造外,编译器还在元数据中产生一个事件定义条目,我们可以通过System.Reflection.EventInfo类来获取其中的信息
11.2 侦听事件
构造了委托之后,Fax对象使用C#的+=操作符登记MailManager的MailMsg事件,编译器会自动将操作转换为对add_MailMsg方法的调用。与之相对应的-=操作符用来注销事件,编译器会将其转换为对事件的remove方法的一个调用
即使我们使用的编程语言不支持事件,我们仍可以通过显式调用add和remove访问器方法来登记和注销事件。但C#要求我们只能使用+=和-=操作符来在委托链表上添加和移除委托实例,而不允许显式调用add和remove方法
11.3 显式控制事件注册
编译器自动产生的add和remove方法会检查线程安全,但如果我们知道应用程序是在单线程下运行,就可以考虑显式实现自己的add和remove方法以提高性能
显式实现需要显示定义一个私有委托链表字段,并在事件中显式定义add和remove访问器方法,它们就与定义事件时编译器默认实现的3个构造相对应。访问器方法的定义与属性类似,但不同的是事件的两个方法都必须存在
11.4 在一个类型中定义多个事件
当类型中事件很多时,每创建一个实例都会为每个事件自动产生委托字段。可以让每个对象都只保存一个事件/委托对的集合,减少内存消耗
文中的演示代码用了EventHandlerSet类型,这是作者自己定义的类型,里面用了一个散列表来存储事件/委托对。每个事件需要定义一个静态只读的Object对象,这个对象的散列码用作在集合中标识事件的键
11.5 设计EventHandlerSet类型
这个类型主要是在一个散列表上的操作,其中的Fire方法用到了System.Delegate的DynamicInvoke方法,并将回调方法的参数组合为一个对象数组传给它,这个方法在内部会检测回调方法所期望的参数与我们传递的参数是否匹配
FCL中定义有一个名为System.ComponentModel.EventHandlerList的类型,该类型和作者定义的EventHandlerSet所做的事情基本是一样的,不同之处在于EventHandlerList内部使用的是一个链表而不是散列表,并且它没有提供任何的线程安全的访问方式。这里有一篇文章展示了一个应用EventHandlerList在类型中定义多个事件的示例,转自CSDN上sayo的博客:http://blog.csdn.net/sayo/archive/2004/07/05/34174.aspx