关于C#实现事件驱动机制的研究

2010-08-15 00:43韩志强
赤峰学院学报·自然科学版 2010年12期
关键词:运算符发布者关键字

韩志强

(赤峰学院 计算机科学与技术系,内蒙古 赤峰 024000)

关于C#实现事件驱动机制的研究

韩志强

(赤峰学院 计算机科学与技术系,内蒙古 赤峰 024000)

事件驱动机制是利用回调函数(低层的函数库调用高层的应用程序)通过控制反转而实现的.事件驱动机制最重要的两个特征是被动性和异步性.在C#中可以通过委托实现回调函数,从而实现控制反转,进而实现事件驱动机制.

事件驱动;回调函数;事件;观察者模式

随着.NET平台的不断推广,现支持.NET开发的编程语言越来越多.但在众多编程语言中,只有C#是微软公司从一开始就专门针对.NET平台的CLR进行设计的语言.由于.NET平台需要Windows操作系统的支持,这就要求其支持的语言都必须接受Windows事件驱动的运行机制,C#也不例外.本文下面将对C#是如何实现事件驱动模型这方面的内容进行探究.

1 事件和消息

广义上讲,事件也称为消息,是程序中令人关注的信息状态上的变化,其含义比较广泛;在基于事件驱动的系统中,事件分为内建事件(包括底层事件和语义事件)和自定义事件.但是严格说来消息与事件不是同一个概念.消息是Windows内部最基本的通讯方式,事件需要通过消息来传递,是消息的主要来源.每当用户触发一个事件,如移动鼠标或敲击键盘,系统都会将其转化为消息并放入其相应程序的消息队列中.

2 事件驱动模型

为了了解事件驱动模型,我们先看看利用win 32的API在windows下创建一个简单窗口的步骤:

①创建一个用于注册事件处理器的事件源(发生事件时,能发出消息到应用程序的消息队列)

②编写实现消息循环的事件管理器(从消息队列获取消息,并对消息进行解释分析,然后调用相应的事件处理器)

③编写事件处理器(根据消息,被系统函数调用并执行相应的处理操作)

通过对上述步骤进行分析,我们可以得到这样的结论,事件驱动机制是利用回调函数(低层的函数库调用高层的应用程序)通过控制反转而实现的.事件驱动式最重要的两个特征是被动性和异步性.被动性来自控制反转,异步性来自会话切换.

在支持事件驱动的开发环境中,消息循环是现成的.许多IDE的图形编辑器在程序开发人员点击控件后,还能自动生成事件处理器的骨架代码,连注册的步骤也省略了.但我们要知道事件驱动机制并不局限于GUI应用.程序开发人员有时须自行设计整个事件系统,他需要决定:采用事件驱动机制是否合适?如果合适,如何设计事件机制?其中包括事件定义、事件触发、事件侦查、事件转化、事件合并、事件调度、事件传播、事件处理等一系列问题.

3 C#如何实现观察者模式——一个典型的事件驱动模型

观察者模式又名发布/订阅模式,既是事件驱动模型的简化,也是事件驱动模型的核心思想.该模式省略了事件管理器部分,由事件源直接调用事件处理器的接口.这样更加简明易用,但威力有所削弱,缺少事件管理、事件连带等机制.下面将以观察者模式为例,说明C#是如何实现事件驱动机制的.

在C#中可以通过委托(delegate)实现回调函数,从而实现控制反转,进而实现事件驱动机制.下面我们将分步介绍其实现步骤:

3.1 定义多个订阅者类

(1)在每个订阅者类的内部都定义能够接收发布者通知的订阅者方法.

(2)作为订阅者方法,他们的参数和返回类型必须与来自于发布者类的委托匹配.

3.2 定义发布者类

(1)在发布者类内定义一个委托类型,委托类型的定义签名和需注册的订阅者方法签名相匹配.

(2)利用已定义的委托类型,定义一个该委托类型的“委托属性”,该属性用来存储注册的订阅者方法列表.在发布者类中,只需一个委托字段即可存储所有订阅者.换言之,来自同一个发布者发布的通知会同时被多个订阅者接收到.

(3)再定义一个成员(属性或方法均可),负责通过“委托属性”调用已注册的多个订阅者方法.在该环节中,必须要判断“委托属性”是否为空,如果“委托属性”为空,在执行时会引发一个Null Reference Exception异常.为了避免这个问题,有必要在触发事件之前检查空值.在这里,我们并不是一开始就检查空值,而是首先将“委托属性”赋给另一个委托变量.这个简单的修改可以确保在检查空值和发送通知之间,假如所有的订阅者都被一个不同的线程移除,也不会触发Null Reference Exception异常.

3.3 然后定义main()方法所在的类,连接订阅者和发布者

(1)对发布者和多个订阅者进行实例化.

(2)向发布者的“委托属性”注册多个订阅者.

(3)然后发布者通过“委托属性”实现调用多个已注册的订阅者方法.

4 使用C#委托来实现观察者模式的缺陷及解决方法

经过上面的步骤我们利用委托实现了观察者模式.但是如果我们仔细分析会发现,委托结构中存在的缺陷可能造成程序人员不经意地引入了一个问题.这个问题和封装有关,即无论事件的订阅方面还是发布方面,都不能得到充分的控制,外部对象可以很容易地影响事件的订阅和发布.为此C#使用关键字event(事件)来解决这些问题.

4.1 在封装订阅方面

利用委托实现的观察者模式中,我们在添加订阅者方法时,可以使用运算符“=”、“+”、“+=”;在取消订阅时,可以使用运算符“-”、“-=”.由于个运算符的作用不同,很容易出现错用运算符的问题.所以有必要对运算符的使用作出限制.为此C#提供的event(事件)对运算符作出限制,要求外部类只能使用“+=”添加订阅,也只能使用“-=”取消订阅.

4.2 在封装发布方面

在上述实现的观察者模式中,已注册的多个订阅者方法“委托属性”,除了可以被发布者调用外,还可以被其它的对象所调用.这会产生这样一个问题:当所有的订阅者都接到一个通知是,但这个通知却可能不是发布者发布的,而是其它对象发布的.为了解决此问题,C#提供的event(事件)要求只有包容类(发布者)才能触发(发布)一个事件通知.

C#利用关键字event完美地解决了上述的问题.因此我们可以对“利用委托实现的观察者模式”进行一下改进,把“委托属性”移除,改为声明一个添加了关键字event的public事件.

5 C#事件的内部机制探析

上面提到了C#利用event(事件)达到了解决问题的目的.但事件在C#内部是如何实现的呢?

事件实际上是一个特殊的委托;当C#编译器获取到带有event修饰符的public委托变量时,首先将委托声明为private;然后,还添加了两个方法和两个特殊的事件块.因此从本质上说,event关键字是C#编译器用于生成恰当封装逻辑的一个C#快捷方式.

当C#编译器遇到event关键字,就会自动对其代码进行扩展,生成与之等价的CIL代码.具体实现的内容为:

(1)C#编译器在获取原始事件定义后,首先在原位置上定义一个private委托变量.这样一来,该委托在任何外部类都无法使用——即使是在从它派生的类中.

(2)接着编译定义两个方法,即add_XXX()和remove_XXX().其中,XXX后缀是从原始事件名中截取的.这两个方法分别负责实现“+=”、“-=”赋值运算符.

(3)最后定义了用event 关键字声明的两个事件块,其语法与属性的getter 和setter 非常类似,只是方法名变成了add 和remove.其中add 块负责处理“+=”运算符,它将调用add_XXX()方法实现添加订阅;remove 块负责处理“-=”运算符,它将调用 remove_XXX()方法实现取消订阅.

这里我们要注意,在最终的CIL代码中,仍然保留了event关键字.换言之,事件是CIL代码能够识别的一样东西,他并不只是一个C#构造.在CIL代码中保留一个等价的event关键字之后,所有语言和编译器都能将事件识别为一个特殊的类成员,并正确处理它.

6 结束语

通过上面的叙述,我们看到C#通过event关键字提供了必要的封装来防止外部类发布一个通知或者取消之前的订阅者.解决了普通委托存在的两个问题,这是C#提供event关键字的关键原因之一.所以利用C#实现事件驱动机制的最佳做法就是使用event(事件).

TP 311

A

1673-260X(2010)12-0050-02

猜你喜欢
运算符发布者关键字
履职尽责求实效 真抓实干勇作为——十个关键字,盘点江苏统战的2021
老祖传授基本运算符
新加坡新法规引争议
成功避开“关键字”
用手机插头的思路学习布尔运算符
基于NDN的高效发布/订阅系统设计与实现
广告发布者的著作权审查义务问题研究
加权映射匹配方法的站内搜索引擎设计
表达式求值及符号推导
C++中运算符的重载应用