NET CLR via C#读书笔记 - 第十一章 事件
1 事件简述
事件的本质是委托,事件是回调机制的一种应用。
定义了事件成员的类型或者类型的实例可以通过其它对象发生了特定的通知。
2 如何定义事件
事件的定义可以大致分为以下4步:
① 定义类型来实现需要发送给事件接收者的附加消息。
② 定义事件成员。
③ 定义负责引发事件的方法来通知事件的登记对象。
④ 定义方法将输入转化为期望事件。
2.1 定义类型来实现需要发送给事件接收者的附加消息
事件引发时,通常需要向事件接收者传递一些附加信息,这种附加信息一般单独封装为独立的类型,类型一般包含一组私有字段以及提供公开这组字段的公共只读属性或者方法。事实上,按照约定,所有的事件的附加信息都应该从EventArgs派生,且派生类应该用EventArgs结尾,因为我们希望一看就知道这是个事件附加信息参数的类型,而不是其它的类型。EventArgs的定义如下:
public class EventArgs {
public static readonly EventArgs Empty = new EventArgs();
public EventArgs() {}
}
该类有一个静态只读字段Empty,这是一个单例;与String.Empty一样,当我们需要一个空的EventArgs时,应该使EventArgs.Empty,而不是重新去派生一个类型,而该类型实际上什么都不做。
在此处我们派生一个附加信息的类型,该类型描述了新邮件到来时,我们需要传递附加信息:
class NewMailEventArgs : EventArgs
{
private readonly string m_From; //附加信息 发件人
private readonly string m_To; //附加信息 收件人
private readonly string m_Subject; //附加信息 主题
//定义只读属性
public string From { get { return m_From; } }
public string To { get { return m_To; } }
public string Content { get { return m_Subject; } }
public NewMailEventArgs(string From,string To,string Subject)
{
this.m_From = From;
this.m_To = To;
this.m_Subject = Subject;
}
}
2.2 定义事件成员
C# 使用 event 关键字定义事件,事件类型的可访问性一般都是public,以下在MailManager类中定义了一个NewMail事件:
internal class MailManager{
public event EventHandler<NewMailEventArgs> NewMail;
//...省略邮件管理类的其它相关代码
}
NewMail是一个EventHandler委托(委托又是引用类型),只不过用了event关键字进行了修饰。
委托是用来包装回调函数的,它的本质就是一个class,回调函数的签名必须与委托的签名一致。一个事件可以有多个处理函数,一个函数就会被包装成一个委托对象,所有的委托对象都保存在NewMail的委托链当中。所以,触发NewMail事件,其实就是遍历其指向的委托对象的委托链,执行每个委托对象所包装的方法。
关于委托的更多信息请点击此处查看
EventHandler 是个泛型委托,他的定义如下:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
所以实际上回调函数的原型如下:
public void FuncName(object sender,NewMailEventArgs e);
对于参数sender使用object而不是使用对应的具体类型的原因如下:
① 主要原因:继承,假如有SmtpMailManager继承了MailManager,那么回调方法的参数应该是SmtpMailManager,而不是MailManager,但事实上SmtpMailManager时继承了实际NewMail的,如果需要由SmtpMailManager引发事件,那么就需要将SmtpMailManager转换为MailManager,由于这一层关系,反正最后都要进行转换,所以直接将类型定义为object。
② 次要原因:灵活性,假如有一个类不是从MailManager派生,也可以使用这个委托。
2.3 定义负责引发事件的方法来通知事件的登记对象
2.3.1 基本实现
代码定义如下:
internal class MailManager
{
//第二步 定义事件消息
public event EventHandler<NewMailEventArgs> NewMail;
//第三步 定义负责引发事件的方法来通知事件的登记对象
protected virtual void OnNewMail(NewMailEventArgs e)
{
EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);
if (temp != null)
{
temp(this, e);
}
}
//...省略邮件管理类的其它相关代码
}
EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);
之所以这样实现代码 ,主要有以下两个原因:
① 如果不使用temp局部变量接收NewMail的引用复制,可能会出现竞态问题,竞态问题在这里的体现是在一个线程中检查完temp != null后,另一个线程可能把该委托移出委托链,这样执行到temp(this, e);时会抛出NullReferenceException异常。
② 如果仅使用 EventHandler temp = NewMail;也会出现问题,这个问题可能会由编译器的优化引起,编译器会通过移出局部变量来优化上述代码,此时就和原因①中描述的场景一样了。
所以最终的用法是 EventHandler temp = Volatile.Read(ref NewMail); Volatile.Read的调用强迫NewMail在语句执行时去读取,不允许编译器进行优化(Ps:作者成书的时间是2012年,发展到今天,编译器已经足够智能,即使用原因2中的写法,编译器也不会去进行优化,不过最稳妥的还是代码示例的写法。)。
2.3.2 扩展方法封装
internal class MailManager
{
//第二步 定义事件消息
public event EventHandler<NewMailEventArgs> NewMail;
//第三步 定义负责引发事件的方法来通知事件的登记对象
protected virtual void OnNewMail(NewMailEventArgs e)
{
e.Raise(this, ref NewMail); //第四版 此处写的是m_NewMail
}
//...省略邮件管理类的其它相关代码
}
public static class EventArgExtensions
{
public static void Raise<TEventArgs> (this TEventArgs e,object sender,ref EventHandler<TEventArgs> evevtDelegate)
{
// 出于线程安全考虑
EventHandler<TEventArgs> temp = Volatile.Read(ref evevtDelegate);
//任何事件定义了对事件的关注就通知他们
if (temp != null)
{
temp(sender, e);
}
}
}
2.4 定义方法将输入转化为期望事件
internal class MailManager
{
//第二步 定义事件消息
public event EventHandler<NewMailEventArgs> NewMail;
//第三步 定义负责引发事件的方法来通知事件的登记对象
protected virtual void OnNewMail(NewMailEventArgs e)
{
e.Raise(this, ref NewMail); //第四版 此处写的是m_NewMail
}
//第四步 定义方法将输入转化为期望事件
public void SimulateNewMail(string From,string to,string subject)
{
// 构造一个事件消息对象来存储需要通知接受者的信息
NewMailEventArgs e = new NewMailEventArgs(From, to, subject);
//调用虚方法通知对象事件已发生
//如果没有重写,那么该方法将通知事件的所有已登记对象
OnNewMail(e);
}
//...省略邮件管理类的其它相关代码
}
3 编译器事件实现浅析
public event EventHandler<NewMailEventArgs> NewMail;
C#编译器在编译时会进行如下转换:
//1 定义一个私有委托字段并初始化为null
private EventHandler<NewMailEventArgs> NewMail = null;
//2 一个公共add_Xxx方法 Xxx是事件名
public void add_NewMail(EventHandler<NewMailEventArgs> value)
{
EventHandler<NewMailEventArgs> preHandler;
EventHandler<NewMailEventArgs> newMail = this.NewMail;
do{
preHandler = newMail;
EventHandler<NewMailEventArgs> newHandler = (EventHandler<NewMailEventArgs>)Delegate.Combine(preHandler,value);
newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail,newHandler,preHandler);
}while(newMail != preHandler);
}
//3 一个公共remove_Xxx方法 Xxx是事件名
public void remove_NewMail(EventHandler<NewMailEventArgs> value)
{
EventHandler<NewMailEventArgs> preHandler;
EventHandler<NewMailEventArgs> newMail = this.NewMail;
do{
preHandler = newMail;
EventHandler<NewMailEventArgs> newHandler = (EventHandler<NewMailEventArgs>)Delegate.Remove(preHandler,value);
newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail,newHandler,preHandler);
}while(newMail != preHandler);
}
第一步中,构造具有恰当委托类型的字段,该字段是对一个委托头部列表的引用,事件发生时会通知列表的委托,初始化为null,表示开始并无侦听者登记对该事件的关注。侦听者对事件进行登记时,其实是将委托类型的一个实例添加到列表中,反之,注销登记事件就是从列表中移除相关委托实例。
第二步是调用System.Delegate的Combine方法,它将委托实例添加到委托列表中,返回新的列表头,并存回字段中。
第三步是调用System.Delegate的Remove方法,它将委托实例从委托列表中删除,返回新的列表头,并存回字段中。
4 设计侦听事件的类型
第2节中说明的是事件定义类的实现,在第四节是说明对事件进行使用的类型,也就是侦听类型的实现。
internal sealed class Fax
{
//登记对事件的关注
public Fax(MailManager mm)
{
mm.NewMail += FaxMsg;
}
//消息处理
private void FaxMsg(object sender,NewMailEventArgs e)
{
Console.WriteLine("Faxing mail massage:");
Console.WriteLine(" From={0},To={1},subject={2}",e.From,e.To,e.Content);
}
//撤销对事件的关注
public void Unregister(MailManager mm)
{
mm.NewMail -= FaxMsg;
}
}
以上内容为对《NET CLR via C#(第四版)》第十一章内容的阅读笔记,只记录其中核心部分内容,书中对以上主题还进行详细的代码演示说明,书中还讨论了如何显示实现事件以便于如何减少内存损耗,如有需要,请详细阅读本书第十一章内容,感谢您的阅读。