定义一个事件成员,表示该类型提供了如下功能:
1.能够在事件中注册方法 2.能够在事件中注销方法 3.当事件发生时,注册的方法会被通知
(事件内部维护了一个注册方法列表)
CLR的事件模型是基于委托的,它可以通过类型安全的方式调用回调方法。而回调方法是订阅事件的对象接收通知的方式。通过一个例子来说明:
①Fax对象的方法注册到MailManager事件 ②Pager对象的方法注册到MailManager事件 ③新的邮件到达MailManager ④MailManager对象向注册的方法发出通知,接收通知的方法可以随意处理。
具体实现步骤如下:
1.定义一个类型,能够hold住任何发送到事件通知接收者的信息
当一个事件被触发,触发事件的对象可能希望发送一些额外的信息给事件通知的接收对象。这些额外的信息需要封装在它自己的类中,根据约定该类需要从System.EventArgs类派生,并且命名以EventArgs结尾。这里定义一个NewMailEventArgs类:
public class NewMailEventArgs : EventArgs { private readonly String m_from, m_to, m_subject; public NewMailEventArgs(String from, String to, String subject) { m_from = from; m_to = to; m_subject = subject; } public String From { get { return m_from; } } public String To { get { return m_to; } } public String Subject { get { return m_subject; } } }
关于EventArgs
[ComVisible(true)] [Serializable] public class EventArgs { public readonly static EventArgs Empty; static EventArgs() { EventArgs.Empty = new EventArgs(); } public EventArgs() { } }
这个类没有实际的用途,只是作为一个基类让其他对象继承。很多对象不需要传递额外的信息,例如按钮事件,只是调用一个回调方法就够了。当我们定义的事件不需要传递额外的信息时,这时调用EventArgs.Empty就行了,不需要重新构建一个EventArgs对象。
2.定义事件成员
public class MailManager
{
...
//NewMail事件名,
//EventHanlder<NewMailEventArgs>,所有的事件通知接收对象必须提供给该委托类型匹配的回调方法
public event EventHandler<NewMailEventArgs> NewMail;
}
System.EventHandler委托的定义为:public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e) where TEventArgs: EventArgs;
为什么这里第一个参数sender的类型是Object?毕竟MailManager类型是唯一触发这个事件的,所以可以设计成这样:
void MethodName(MailManager sender,NewMailEventArgs e)
这种情况会有一个弊端,当sender是SmtpMailManager时,回调方法也需要改变,使用Object能够很好的兼容。定义回调方法的参数名约定为e,这样做主要是为了保持一致性。方便开发人员。
事件机制要求所有的事件处理方法必须返回void,这是必要的,因为一个事件可能触发很多的回调方法,没有办法获取所有的返回值,索性就不允许返回值,全部为void。有些FCL里面的事件处理程序没有遵循,而是返回了一个Assembly类型。
3.定义一个方法来响应事件的发生
按照惯例,这个类应该定义一个protected,virtual的方法供内部的代码调用。这个方法接收一个NewMailEventArgs对象,这个对象包含要传递给消息接收方的一些信息。如下:
protected virtual void OnNewMail(NewMailEventArgs e)
{
//复制一个委托的引用到临时字段temp,这样确保线程安全
EventHandler<NewMailEventArgs> temp = Interlocked.CompareExchange(ref NewMail, null, null);
//任何注册到事件里面的方法,通知它们
if (temp != null)
{
temp(this, e);
}
}
Tips:使用线程安全的方式触发事件(①——>④为不断改进的过程)
①当.NET第一次推出的时候,给开发者推荐的事件触发方式如下:
//v1.0
protected virtual void OnNewMail(NewMailEventArgs e)
{
if (NewMail != null)
{
NewMail(this, e);
}
}
弊端:这里检查了NewMail不为null才触发,但是当检查完之后,在调用NewMail之前,有其他的线程从委托链中移除了一个委托,使得NewMail为null,此时会抛出异常。
②先将NewMail用一个临时变量存起来,这时就不会因为调用时被其他线程修改而抛出异常。之所以能够这样做,是因为委托类型跟字符串类型一样是不可变的。
//v2.0
protected void OnNewMail(NewMailEventArgs e)
{
EventHandler<NewMailEventArgs> temp = NewMail;
if (temp != null)
{
temp(this, e);
}
}
弊端:可能被编译器优化掉本地temp变量,如果发生这种情况,就回到了第一种了。
③修复上面的bug,如下:
//v3.0
protected void OnNewMail(NewMailEventArgs e)
{
EventHandler<NewMailEventArgs> temp = Thread.VolatileRead(ref NewMail);
if (temp != null)
{
temp(this, e);
}
}
这里使用VolatileRead会强制读取temp的值,但是这里不能这样写,编译不通过。但是有一个Interlocked.CompareExchange可以使用:
④
//v4.0
protected virtual void OnNewMail(NewMailEventArgs e)
{
//复制一个委托的引用到临时字段temp,这样确保线程安全
EventHandler<NewMailEventArgs> temp = Interlocked.CompareExchange(ref NewMail, null, null);
//任何注册到事件里面的方法,通知它们
if (temp != null)
{
temp(this, e);
}
}
如果NewMail为null,CompareExchange将NewMail的值改变为null,如果不为null则返回原值。换句话说,CompareExchange不会改变NewMail的值,只是以线程安全的方式返回NewMail的值,这里是一个原子操作。
第④个版本是最佳的,技术上最正确的版本。实际开发中还是可以使用第②个版本,因为JIT编译器能够识别这种模式而不去优化本地的temp变量。特别地,所有微软的JIT编译器都遵循不会对堆引入新的读取,因此缓存一个引用在本地变量可以确保堆引用只被访问一次(这是没有写入文档的,理论上,还是可能发生变化,所以最好选用第④版本。)
为了方便可以定义一个扩展方法来封装:
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.CompareExchange(ref eventDelegate, null, null);
if (temp != null)
{
temp(sender, e);
}
}
}
然后可以重写OnNewMail:
protected virtual void OnNewMail(NewMailEventArgs e) { e.Raise(this, ref NewMail); }
4.定义一个方法用来传递一些输入到事件
public void SimulateNewMail(String from, String to, String subject) { NewMailEventArgs e = new NewMailEventArgs(from, to, subject); OnNewMail(e); }
本文导航
- 第1页: 首页
- 第2页: 编译器是怎么实现事件的?
- 第3页: 定义类型监听事件