事件这一名称对于我们.NET码农来说肯定不会陌生,各种技术框架例如WindowsForm、ASP.NET WebForm都会有事件这一名词,并且所有的定义都基本相同。在.NET中,事件和委托在本质上并没有太多的差异,实际环境下事件的运用却比委托更加广泛。
在Microsoft的产品文档上这样来定义的事件:事件是一种使对象或类能够提供通知的成员。客户端可以通过提供事件处理程序为相应的事件添加可执行代码。设计和使用事件的全过程大概包括以下几个步骤:
下面我们来按照规范的步骤来展示一个通过控制台输出事件的使用示例:
① 定义一个控制台事件ConsoleEvent的参数类型ConsoleEventArgs
② 定义一个控制台事件的管理者,在其中定义了事件类型的私有成员ConsoleEvent,并定义了事件的发送方法SendConsoleEvent
③ 定义了事件的订阅者Log,在其中通过控制台时间的管理类公开的事件成员订阅其输出事件ConsoleEvent
④ 在Main方法中进行测试:
当该程序执行时,ConsoleManager负责在控制台输出测试的字符串消息,与此同时,订阅了控制台输出事件的Log类对象会在指定的日志文件中写入这些字符串消息。可以看出,这是一个典型的观察者模式的应用,也可以说事件为观察者模式提供了便利的实现基础。
事件的定义和使用方式与委托极其类似,那么二者又是何关系呢?经常听人说,委托本质是一个类型,而事件本质是一个特殊的委托类型的实例。关于这个解释,最好的办法莫过于通过查看原代码和编译后的IL代码进行分析。
① 回顾刚刚的代码,在ConsoleManager类中定义了一个事件成员
EventHandler是.NET框架中提供的一种标准的事件模式,它是一个特殊的泛型委托类型,通过查看元数据可以验证这一点:
正如上面代码所示,我们定义一个事件时,实际上是定义了一个特定的委托成员实例。该委托没有返回值,并且有两个参数:一个事件源和一个事件参数。而当事件的使用者订阅该事件时,其本质就是将事件的处理方法加入到委托链之中。
② 下面通过Reflector来查看一下事件ConsoleEvent的IL代码(中间代码),可以更方便地看到这一点:
首先,查看EventHandler的IL代码,可以看到在C#编译器编译delegate代码时,编译后是成为了一个class。
其次,当C#编译器编译event代码时,会首先为类型添加一个EventHandler<T>的委托实例对象,然后为其增加一对add/remove方法用来实现从委托链中添加和移除方法的功能。
通过查看add_ConsoleEvent的IL代码,可以清楚地看到订阅事件的本质是调用Delegate的Combine方法将事件处理方法绑定到委托链中。
Summary:事件是一个特殊的委托实例,提供了两个供订阅事件和取消订阅的方法:add_event和remove_event,其本质都是基于委托链来实现。
多事件的类型在实际应用中并不少见,尤其是在一些用户界面的类型中(例如在WindowsForm中的各种控件)。这些类型动辄将包含数十个事件,如果为每一个事件都添加一个事件成员,将导致无论使用者是否用到所有事件,每个类型对象都将占有很大的内存,那么对于系统的性能影响将不言而喻。事实上,.NET的开发小组运用了一种比较巧妙的方式来避免这一困境。
Solution:当某个类型具有相对较多的事件时,我们可以考虑显示地设计订阅、取消订阅事件的方法,并且把所有的委托链表存储在一个集合之中。这样做就能避免在类型中定义大量的委托成员而导致类型过大。
下面通过一个具体的实例来说明这一设计:
① 定义包含大量事件的类型之一:使用EventHandlerList成员来存储所有事件
② 定义包含大量事件的类型之二:申明多个具体的事件
③ 定义事件的订阅者(它对多事件类型内部的构造一无所知)
④ 编写入口方法来测试多事件的触发
最终运行结果如下图所示:
总结EventHandlerList的用法,在多事件类型中为每一个事件都定义了一套成员,包括事件的委托原型、事件的订阅和取消订阅方法,在实际应用中,可能需要定义事件专用的参数类型。这样的设计主旨在于改动包含多事件的类型,而订阅事件的客户并不会察觉这样的改动。设计本身不在于减少代码量,而在于有效减少多事件类型对象的大小。
这是一个典型的观察者模式的应用场景,事件的发源在于猫叫这个动作,在猫叫之后,老鼠开始逃跑,而主人则会从睡梦中惊醒。可以发现,主人和老鼠这两个类型的动作相互之间没有联系,但都是由猫叫这一事件触发的。
设计的大致思路在于,猫类包含并维护一个猫叫的动作,主人和老鼠的对象实例需要订阅猫叫这一事件,保证猫叫这一事件发生时主人和老鼠可以执行相应的动作。
(1)设计猫类,为其定义一个猫叫的事件CatCryEvent:
(2)设计老鼠类,在其构造方法中订阅猫叫事件,并提供对应的处理方法
(3)设计主人类,在其构造犯法中订阅猫叫事件,并提供对应的处理方法
(4)最后在Main方法中进行场景的模拟:
这里定义了一只猫,两只老鼠与一个主人,当猫的CatCry方法被执行到时,会触发猫叫事件CatCryEvent,此时就会通知所有这一事件的订阅者。本场景的关键之处就在于主人和老鼠的动作应该完全由猫叫来触发。下面是场景模拟代码的运行结果: