C#系列文章之事件

文件涉及的内容:

  • 设计公开事件类型
  • 编译器如何实现事件
  • 设计侦听事件的类型
  • 显式实现事件

事件:定义了事件成员的类型允许类型通知其他对象发生特定的事情。

CLR事件模型以委托为基础,委托是调用回调方法的一种类型安全的方式,对象凭借调用方法接收他们订阅的通知。

定义了事件成员的类型要求能够提供以下功能:

  1. 方法能登记它对事件的关注
  2. 方法能注销它对事件的关注
  3. 事件发生时,登记的方法将收到通知

本文章以一个电子邮件应用程序为例。当电子邮件到达时,用户希望将邮件转发给传真机或寻呼机进行处理。先设计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         }
时间: 2024-08-25 00:16:17

C#系列文章之事件的相关文章

【强烈强烈推荐】《ORACLE PL/SQL编程详解》全原创(共八篇)--系列文章导航

原文:[强烈强烈推荐]<ORACLE PL/SQL编程详解>全原创(共八篇)--系列文章导航 <ORACLE PL/SQL编程详解> 系列文章目录导航 ——通过知识共享树立个人品牌. 本是成书的,但后来做其他事了,就无偿的贡献出来,被读者夸其目前为止最“实在.经典”的写ORACLE PL/SQL编程的文章-! 觉得对你有帮助,请留言与猛点推荐,谢谢. [推荐]ORACLE PL/SQL编程详解之一:PL/SQL 程序设计简介(千里之行,始于足下) 本篇主要内容如下:第一章 PL/S

优化网站设计系列文章总结和导读

摘自:http://www.cnblogs.com/chenxizhang/archive/2013/05/20/3088196.html 概述 其实想写这方面的文章由来已久,这个系列文章的想法是参照雅虎团队提供的35条性能优化的最佳实践(其实最早的时候是14条),再结合我自己多年的实际工作经验,结合具体的开发平台(ASP.NET),为读者提供既有理论知识.又有实践指导的参考资料,对于优化而言,本身是一个长期细致的工作(没有所谓的银弹),并且重在权衡利弊,选择最适合自己项目情况的解决方案.而要达

1、HTML学习 - IT软件人员学习系列文章

本文做为<IT软件人员学习系列文章>的第一篇,将从最基本的开始进行描述,了解的人完全可以跳过本文(后面会介绍一些工具). 今天讲讲Web开发中最基础的内容:HTML(超文本标记语言).HTML的内容是文本,就象我们在文本文档中记录内容一样. 一.最基本例子. 下面是一个最基本的HTML页面文本内容(代码一): <html> <head> <title>这个HTML页面</title> </head> <body> 这是HT

重新想象 Windows 8.1 Store Apps 系列文章索引

[源码下载] [重新想象 Windows 8 Store Apps 系列文章] 作者:webabcd 1.重新想象 Windows 8.1 Store Apps (72) - 新增控件: AppBar, CommandBar 介绍重新想象 Windows 8.1 Store Apps 之新增控件 AppBar - 应用程序栏控件(新增了 AppBarButton, AppBarToggleButton, AppBarSeparator) CommandBar - 应用程序栏控件(AppBar 简

使用 jQuery Mobile 与 HTML5 开发 Web App 系列文章目录

使用 jQuery Mobile 与 HTML5 开发 Web App 系列文章目录 时间:2012年9月20日 分类:JavaScript 标签:HTML5‚ jQuery Mobile‚ Web App “使用 jQuery Mobile 与 HTML5 开发 Web App”系列文章的数目累积起来也比较多了,为方便大家浏览, Kayo 把这些文章整理成一个目录,收录那些已经写好的文章并会继续更新. 该系列的文章实质上分成四个部分,分别是总体概况.jQuery Mobile 组件.jQuer

【NLP】蓦然回首:谈谈学习模型的评估系列文章(三)

基于NLP角度的模型评价方法 作者:白宁超 2016年7月19日19:04:51 摘要:写本文的初衷源于基于HMM模型序列标注的一个实验,实验完成之后,迫切想知道采用的序列标注模型的好坏,有哪些指标可以度量.于是,就产生了对这一专题进度学习总结,这样也便于其他人参考,节约大家的时间.本文依旧旨在简明扼要梳理出模型评估核心指标,重点达到实用.本文布局如下:第一章采用统计学习角度介绍什么是学习模型以及如何选择,因为现今的自然语言处理方面大都采用概率统计完成的,事实证明这也比规则的方法好.第二章采用基

C#网络编程系列文章(一)之Socket实现异步TCPserver

原创性声明 本文作者:小竹zz 本文地址http://blog.csdn.net/zhujunxxxxx/article/details/44258719 转载请注明出处 文章系列文件夹 C#网络编程系列文章(一)之Socket实现异步TCPserver C#网络编程系列文章(二)之Socket实现同步TCPserver C#网络编程系列文章(三)之TcpListener实现异步TCPserver C#网络编程系列文章(四)之TcpListener实现同步TCPserver C#网络编程系列文章

领域驱动设计系列文章(1)——通过现实例子显示领域驱动设计的威力(转)

http://www.blogjava.net/johnnylzb/archive/2010/05/15/321057.html 领域驱动设计系列文章(1)——通过现实例子显示领域驱动设计的威力 曾经参与过系统维护或是在现有系统中进行迭代开发的软件工程师们,你们是否有过这样的痛苦经历:当需要修改一个Bug的时候,面对一个类中成百上千行的代码,没有注释,千奇百怪的方法和变量名字,层层嵌套的方法调用,混乱不堪的结构,不要说准确找到Bug所在的位置,就是要清晰知道一段代码究竟是做了什么也非常困难,最终

C#网络编程系列文章(八)之UdpClient实现同步UDP服务器

原创性声明 本文作者:小竹zz 本文地址http://blog.csdn.net/zhujunxxxxx/article/details/44258719 转载请注明出处 文章系列目录 C#网络编程系列文章(一)之Socket实现异步TCP服务器 C#网络编程系列文章(二)之Socket实现同步TCP服务器 C#网络编程系列文章(三)之TcpListener实现异步TCP服务器 C#网络编程系列文章(四)之TcpListener实现同步TCP服务器 C#网络编程系列文章(五)之Socket实现异