购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

4.2 事件

当使用委托时,一般会出现广播者(broadcaster)和订阅者(subscriber)两种角色。

广播者是包含委托字段的类型,它通过调用委托决定何时进行广播。

订阅者是方法的目标接收者。订阅者通过在广播者的委托上调用 += -= 来决定何时开始监听和何时监听结束。订阅者不知道也不会干涉其他订阅者。

事件就是正式定义这一模式的语言功能。事件是一种使用有限的委托功能实现广播者/订阅者模型的结构。使用事件的主要目的在于保证订阅者之间不互相影响。

声明事件最简单的方法是在委托成员的前面加上 event 关键字:

Broadcaster 类型中的代码对 PriceChanged 有完全的访问权限,并可以将其视为委托。而 Broadcaster 类型之外的代码则仅可以在 PriceChanged 事件上执行 += -= 运算。

事件的工作机制是怎样的

当声明如下委托时,在内部发生了三件事情:

首先,编译器将事件的声明转换为如下形式:

add remove 关键字明确了事件的访问器,就像属性的访问器那样。我们将在后续讲解如何编写访问器。

而后,编译器在 Broadcaster 类里面找到除调用 += -= 之外的 PriceChanged 引用点,并将它们重定向到内部的 priceChanged 委托字段。

最后,编译器对事件上的 += -= 运算符操作相应地调用事件的 add remove 访问器。有意思的是当应用于事件时, += -= 的行为是唯一的,而不像其他情况下是 + - 运算符与赋值运算符的简写。

观察下面的例子,在 Stock 类中,每当 Stock Price 发生变化时,就会触发 Price-Changed 事件:

在上例中,如果将 event 关键字去掉, PriceChanged 就变成了普通的委托字段,虽然运行结果是不变的,但是 Stock 类就没有原来健壮了。因为这时订阅者可以通过以下方式相互影响:

· 通过重新指派 PriceChanged 替换其他订阅者(不用 += 运算符)。

· 清除所有订阅者(将 PriceChanged 设置为 null )。

· 通过调用委托广播到其他订阅者。

4.2.1 标准事件模式

在.NET程序库中几乎所有和事件相关的定义中,都体现了一个标准模式。该模式保证了程序库和用户代码使用事件的一致性。标准事件模式的核心是 System.EventArgs 类,这是一个预定义的没有成员(但是有一个静态的 Empty 字段)的类。 EventArgs 是为事件传递信息的基类。在 Stock 示例中,我们可以继承 EventArgs 以便在 PriceChanged 事件被触发时传递新的和旧的Price值:

考虑到复用性, EventArgs 子类应当根据它包含的信息来命名(而非根据使用它的事件命名)。它一般将数据以属性或只读字段的方式暴露给外界。

EventArgs 子类就位后,下一步就是选择或者定义事件的委托了。这一步需要遵循三条规则:

· 委托必须以 void 作为返回值。

· 委托必须接受两个参数,第一个参数是 object 类型,第二个参数是 EventArgs 的子类。第一个参数表明了事件的广播者,第二个参数则包含了需要传递的额外信息。

· 委托的名称必须以 EventHandler 结尾。

.NET定义了一个名为 System.EventHandler<> 的泛型委托来辅助实现标准事件模式:

在泛型出现之前(C# 2.0之前),我们只能以如下方式书写自定义委托:

出于历史原因,.NET库中大部分事件使用的委托都是这样定义的。

接下来就是定义选定委托类型的事件了。这里使用泛型的 EventHandler 委托:

最后,该模式需要编写一个protected的虚方法来触发事件。方法名称必须和事件名称一致,以On作为前缀,并接收唯一的 EventArgs 参数:

为了在多线程下可靠地工作(参见第14章),在测试和调用委托之前,需要将它保存在一个临时变量中:

我们可以使用null条件运算符来避免声明临时变量:

这种方式既线程安全又书写简明,是现阶段最好的事件触发方式。

这样就提供了一个子类可以调用或重写事件的关键点(假设该类不是密封类)。

以下是完整的例子:

如果事件不需要传递额外的信息,则可以使用预定义的非泛型委托 EventHandler 。在本例中,我们重写 Stock 类,当price属性发生变化时,触发 PriceChanged 事件,事件除了传达已发生的消息之外没有必须包含的信息。为了避免创建不必要的 EventArgs 实例,我们使用了 EventArgs.Emtpy 属性:

4.2.2 事件访问器

事件访问器是对事件的 += -= 功能的实现。默认情况下,访问器由编译器隐式实现。考虑如下声明:

编译器将其转化为:

· 一个私有的委托字段。

· 一对公有的事件访问器函数( add_PriceChanged remove_PriceChanged ),它们将 += -= 操作转向了私有的委托字段。

我们也可以显式定义事件访问器来替代这个过程。以下是 PriceChanged 事件的手动实现:

本例在功能上和C#的默认访问器实现是等价的(C#使用无锁的比较-交换算法来保证委托更新时的线程安全性,请参见 http://albahari.com/threading )。若定义了自定义事件访问器,C#就不会生成默认的字段和访问器逻辑了。

显式定义事件访问器,可以在委托的存储和访问上进行更复杂的操作。这主要有以下三种情形:

· 当前事件访问器仅仅是广播事件的类的中继器。

· 当类定义了大量的事件,而大部分事件有很少的订阅者时,例如Windows控件。在这种情况下,最好在一个字典中存储订阅者的委托实例,因为字典会比大量的空委托字段引用更少的存储开销。

· 当显式实现声明事件的接口时。

以下例子展示了第三种情形:

事件的 add remove 部分会分别编译为 add_ XXX remove_ XXX 方法。

4.2.3 事件的修饰符

和方法类似,事件可以是虚的(virtual)、重写的(overridden)、抽象的(abstract)或者密封的(sealed),当然也可以是静态的(static): uNxfkP0jb9G3ciIHtLWLTOObpMeWTJ1lXu+tpkgAfYxrbP4uX2NF21lTbK4sxo7e

点击中间区域
呼出菜单
上一章
目录
下一章
×