0

0

C#系列文章事件

PHPz

PHPz

发布时间:2017-03-12 15:43:26

|

1171人浏览过

|

来源于php中文网

原创

文件涉及的内容:

  • 设计公开事件类型

  • 编译器如何实现事件

  • 设计侦听事件的类型

  • 显式实现事件

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

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 NewMail;
    }

其中NewMail是事件名称。事件成员类型是EventHandler说明事件通知的所有接收者都必须提供一个原型和其委托类型匹配的回调方法。由于泛型System.EventHandler委托类型的定义如下:

       public delegate void EventHandler(Object s定义0er,TEventArgs e);

所以方法原型必须具有以下形式:void MethodName(Object sender,NewMailEventArgs e);之所以事件定义1要求所有定义2程序的返回类型都是void,是因为引发事件后可能要调用好几个回调方法,但没办法获得所有方法的返回值,返回void就不允许回调方法有返回值。

第三步:定义负责引发事件的方法来通知事件的登记对象


 1         ///  2         /// 定义负责引发事件的方法来通知事件的登记对象,该方法定义在MailManager中 3         /// 如果类是密封的,该方法要声明为私有和非虚 4         ///  5         ///  6         protected virtual void OnNewMail(NewMailEventArgs e) 7         { 8             //出于线程安全考虑,现在将委托字段的引用复制到一个临时变量中 9             EventHandler temp = Volatile.Read(ref NewMail);10             if(temp!=null)11             {12                 temp(this, e);13             }14         }

上面方法使用了Volatile.Read()方法确保线程安全,主要考虑下面两种情况:

Yaara
Yaara

使用AI生成一流的文案广告,电子邮件,网站,列表,博客,故事和更多…

下载

1.直接判断NewMail!=null,但在调用NewMail之前,另一个线程可能从委托链中移除了一个委托,使其为空,从而发生(NullReference定义3)异常。

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 NewMail;

定义4编译器会转换为以下代码:


         //一个被初始化为null的私有字段
        private EventHandler NewMail = null;        public void add_NewMail(EventHandler value)
        {            //通过循环和对CompareExchange的调用,以一种线程安全的方式向事件添加委托            //CompareExchange是把目标操作数(第1参数所指向的内存中的数)            //与一个值(第3参数)比较,如果相等,            //则用另一个值(第2参数)与目标操作数(第1参数所指向的内存中的数)交换
            EventHandler prevHandler;
            EventHandler newMail = this.NewMail;            do
            {
                prevHandler = newMail;
                EventHandler newHandler =
                    (EventHandler)Delegate.Combine(prevHandler, value);
                newMail = Interlocked.CompareExchange>(ref this.NewMail, newHandler, prevHandler);
            } while (newMail != prevHandler);
        }        public void remove_NewMail(EventHandler value)
        {
            EventHandler prevHandler;
            EventHandler newMail = this.NewMail;            do
            {
                prevHandler = newMail;
                EventHandler newHandler =
                    (EventHandler)Delegate.Remove(prevHandler, value);
                newMail = Interlocked.CompareExchange>(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);
        }        /// 
        /// 注销        /// 
        /// 
        public void Unregister(MailManager mm)
        {
            mm.NewMail -= FaxMsg;
        }
    }

 电子邮件应用程序初始化时首先构造MailManager对象,并将对该对象的引用保存到变量中。然后构造Fax对象,并将MailManager对象引用作为实参传递。在Fax构造器中,使用+=登记对NewMail事件的关注。

1.4 显式实现事件

对于System.Windows.定义5ms.Control类型定义了大约70个事件。每个从Control派生类型定义6都要浪费大量内存,而大多数我们只关心少数几个事件。如何通过显式实现事件来高效的实现提供了大量事件的类思路如下:

定义事件时:公开事件的每个对象都要维护一个集合(如字典)。集合将某种事件标识符作为健,将委托列表作为值。新对象构造时集合也是空白。登记对一个事件的关注会在集合中查找事件的标识符。如果事件标识符存在,新委托就和这个事件的委托列表合并,否则就添加事件标识符和委托。   

引发事件时:对象引发事件会在集合中查找事件的标识符,如果没有说明没有对象登记对这个事件的关注,所以也没委托需要回调。否则就调用与它关联的委托列表。


 1  public sealed class EventKey { } 2     public sealed class EventSet 3     { 4         //定义私有字典 5         private readonly Dictionary m_events = 6             new Dictionary(); 7         ///  8         /// 不存在添加,存在则和现有EventKey合并 9         ///        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         /// 22         /// 删除委托,在删除最后一个委托时还需删除字典中EventKey->Delegate23         ///        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                 else38                 {39                     m_events.Remove(eventKey);40                 }41             }42             Monitor.Exit(m_events);43         }44         /// 45         /// 为指定的EventKey引发事件46         ///        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 Foo13         {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         }

 

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
Python 序列化
Python 序列化

本专题整合了python序列化、反序列化相关内容,阅读专题下面的文章了解更多详细内容。

0

2026.02.02

AO3官网入口与中文阅读设置 AO3网页版使用与访问
AO3官网入口与中文阅读设置 AO3网页版使用与访问

本专题围绕 Archive of Our Own(AO3)官网入口展开,系统整理 AO3 最新可用官网地址、网页版访问方式、正确打开链接的方法,并详细讲解 AO3 中文界面设置、阅读语言切换及基础使用流程,帮助用户稳定访问 AO3 官网,高效完成中文阅读与作品浏览。

91

2026.02.02

主流快递单号查询入口 实时物流进度一站式追踪专题
主流快递单号查询入口 实时物流进度一站式追踪专题

本专题聚合极兔快递、京东快递、中通快递、圆通快递、韵达快递等主流物流平台的单号查询与运单追踪内容,重点解决单号查询、手机号查物流、官网入口直达、包裹进度实时追踪等高频问题,帮助用户快速获取最新物流状态,提升查件效率与使用体验。

27

2026.02.02

Golang WebAssembly(WASM)开发入门
Golang WebAssembly(WASM)开发入门

本专题系统讲解 Golang 在 WebAssembly(WASM)开发中的实践方法,涵盖 WASM 基础原理、Go 编译到 WASM 的流程、与 JavaScript 的交互方式、性能与体积优化,以及典型应用场景(如前端计算、跨平台模块)。帮助开发者掌握 Go 在新一代 Web 技术栈中的应用能力。

11

2026.02.02

PHP Swoole 高性能服务开发
PHP Swoole 高性能服务开发

本专题聚焦 PHP Swoole 扩展在高性能服务端开发中的应用,系统讲解协程模型、异步IO、TCP/HTTP/WebSocket服务器、进程与任务管理、常驻内存架构设计。通过实战案例,帮助开发者掌握 使用 PHP 构建高并发、低延迟服务端应用的工程化能力。

5

2026.02.02

Java JNI 与本地代码交互实战
Java JNI 与本地代码交互实战

本专题系统讲解 Java 通过 JNI 调用 C/C++ 本地代码的核心机制,涵盖 JNI 基本原理、数据类型映射、内存管理、异常处理、性能优化策略以及典型应用场景(如高性能计算、底层库封装)。通过实战示例,帮助开发者掌握 Java 与本地代码混合开发的完整流程。

5

2026.02.02

go语言 注释编码
go语言 注释编码

本专题整合了go语言注释、注释规范等等内容,阅读专题下面的文章了解更多详细内容。

62

2026.01.31

go语言 math包
go语言 math包

本专题整合了go语言math包相关内容,阅读专题下面的文章了解更多详细内容。

55

2026.01.31

go语言输入函数
go语言输入函数

本专题整合了go语言输入相关教程内容,阅读专题下面的文章了解更多详细内容。

27

2026.01.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
C# 教程
C# 教程

共94课时 | 8.3万人学习

C 教程
C 教程

共75课时 | 4.4万人学习

C++教程
C++教程

共115课时 | 15.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号