一、事件的本质
事件是软件系统里的两个子系统之间,或者两个模块之间,或者两个对象之间发送消息,并处理消息的过程。在面向对象的世界里,就可以统一认为是两个对象之间的行为。
两个对象之间发送的这种消息,对发送方来讲是产生一个事件,对接受方来讲是需要处理某个事件。这种消息可以是用户操作产生的或者软件系统里的某个对象产生的。
对象之间的事件处理
从上图可见,对象一产生一个事件,这个事件发生以后需要对象二执行某种动作。这就是事件机制。对象一是事件的产生者,或者发送者;对象二是事件的接收者或者订阅者。对象一产生某种消息,需要对象二响应并处理这给消息,这就是事件的本质。
以往的很多软件系统都在采用事件机制处理很多问题。例如从最本质的计算机体系中的软中断处理,到masm中的jump,到c/c++中的回调函数等等。只不过越高级的软件系统处理事件或者其提供的很多处理方法越接近人的思维,而越远离机器思维。构建软件系统的方法从本质上就是从机器思维走向人的思维的过程。
二、事件机制的好处
1、直接调用
采用事件机制有什么好处?事件发送者为什么不直接调用事件接受者提供的处理函数呢?
调用机制
如果所示,两个对象之间的调用机制。对象B调用对象A的方法,可以通过函数指针或者跳转(汇编语言)等实现。这种方法造成的结果是A和B的紧密耦合,即B对A有很强的依赖性。可以看成B是事件的发布者,A是事件的响应和处理者。不过这种机制用事件机制解释从理论上就比较牵强了。同一种事物,其实现的思想不一样。
现在假设有个对象C也要响应B的事件。那么,按照上面的这种机制,需求修改对象B的代码,调用对象C的方法。这样机制造成了非常强的依赖关系。代码的修改和扩展非常麻烦。如果对象越多,这种关系越多,整个系统越复杂。如果一个系统里面对象很多,这种依赖关系也很多的情况下,这种调用关系就会十分复杂,对系统的健壮性和优良性会造成影响。
2、回调机制
如果按照c#的委托思想,B需要事先提供对事件处理函数的某些回调指针。这样,其它对象,例如A和C就去修改它的回调指针,把自己的方法联系到上面。但是它们之间的耦合关系就比上面简单了。
回调机制
回调机制的思想已经比较接近委托的概念。其实委托在本质上也就和回调指针差不多,只是概念上更加高级。对象B作为事件的发布者,事先定义一些回调函数指针,然后在本地合适的地方调用这些指针指向的函数。而事件订阅者或者处理者A和C所作的就是让给这些空指针赋值,把自己的事件处理方法赋给它,从而实现B调用A和C的方法。
在 C 或 C++ 中与委托最为相似的是函数指针。然而,函数指针只能引用静态函数,而委托可以引用静态方法和实例方法。当委托引用实例方法时,委托不仅存储对方法入口点的引用,还存储对为其调用该方法的类实例的引用。与函数指针不同,委托是面向对象、类型安全并且安全的。
三、事件机制的实现
1、委托的局限
如果单纯用委托,对于事件的发布者B来说,假设它发布事件e,对于事件e,它目前已经知道有A和C对象需要订阅这个事件。所以,它就申明两个委托对象引用(本质上类似于函数指针),然后让A和C对象来采用类似回调的机制订阅和响应事件。
如果后来,有个对象D也需要订阅B的事件e,它怎么办呢?一种情况是D修改B的一个委托对象引用,把自己的处理方法包装成一个委托对象付给它。这样,D就抢夺了A或者C的订阅。否则,就需要修改B的代码,添加一个类似的委托对象引用,以便让D来使用。
这样做的后果是事件发布者B需要申明很多委托对象的引用变量。结果是弄得代码维护比较混乱,并且使用者也很多,依赖关系也不容易搞清楚,容易发生错误。
2、事件的引入
有了委托,就提供了类似回调一样的功能。但是,回调机制需要事件发布者和事件订阅者双方的共同参与和努力。也就是,每增加一个订阅者,那么发布者对象就需要提供一个委托引用,让订阅者挂钩。
如果事件的发布者发布一个事件以后就不在关心谁来订阅它,那么以后的处理就交给了使用者,而发布者不再关心事件处理者的问题。
订阅机制
C#事件的事件就是这种订阅机制,真正的订阅。发布者不需要关心订阅者。
C#事件给订阅者提供了对事件响应的注册和反注册功能。订阅和撤销完全是事件接受方的行为。
C#事件机制的实现包括以下几步:
1、 事件发布者定义一个委托类型;
2、 事件发布者定义一个事件,并且关联到已经定义的委托上。
3、 事件订阅者需要产生一个委托实例,并把它添加到委托列表。
所以,事件event可以看成是一个事件列表,订阅者可以注册和撤销自己的响应和处理机制,但是它没有办法更改整个列表(原则上)。所以,提供了更强、更安全的方式。
四、事件机制的代码实例
应用程序结构图
如图所示,事件发布对象发布一个事件;事件订阅对象订阅和处理该事件。
using System;
namespace EventExample
{
///<summary>
/// MainClass : 主应用程序类
///</summary>
class MainClass
{
///<summary>
///应用程序的主入口点。
///</summary>
[STAThread]
static void Main(string[] args)
{
EventPublisher publisher = new EventPublisher();
EventReader1 reader1 = new EventReader1(publisher);
EventReader2 reader2 = new EventReader2(publisher);
publisher.DoSomthing();
Console.WriteLine("This program already finished!");
Console.ReadLine();
}
}
///<summary>
/// EventPublisher : 事件的发布者。
///</summary>
public class EventPublisher
{
// 第一步是申明委托
public delegate int sampleEventDelegate(string messageInfo);
// 第二步是申明与上述委托相关的事件
public event sampleEventDelegate sampleEvent;
public EventPublisher()
{
}
public void DoSomthing()
{
/* ... */
// 激发事件
if(this.sampleEvent != null)
{
this.sampleEvent("hello world!");
}
/* ... */
}
}
///<summary>
/// EventReader1 : 事件的订阅者1。
///</summary>
public class EventReader1
{
public EventReader1(EventPublisher publisher)
{
publisher.sampleEvent +=
new EventExample.EventPublisher.sampleEventDelegate(ResponseEvent);
}
private int ResponseEvent(string msg)
{
Console.WriteLine(msg + " --- This is from reader1");
return 0;
}
}
///<summary>
/// EventReader2 : 事件的订阅者2。
///</summary>
public class EventReader2
{
public EventReader2(EventPublisher publisher)
{
publisher.sampleEvent +=
new EventExample.EventPublisher.sampleEventDelegate(ResponseEvent);
publisher.sampleEvent +=
new EventExample.EventPublisher.sampleEventDelegate(ResponseEvent);
}
private int ResponseEvent(string msg)
{
Console.WriteLine(msg + " --- This is from reader2");
Console.WriteLine("Please:down enter key!");
Console.ReadLine();
Console.WriteLine("ok");
return 0;
}
}
}
程序运行结果
总结:事件发布者发布的事件在实质上可以看成对外提供的回调函数指针列表。这个列表的容量可以动态增长。事件订阅者可以把自己的事件注册到这个列表或者撤销注册,但是它从原则上无法更改或者对其它订阅者的注册产生影响。事件发布者通过两种手段使得订阅者正确地使用事件机制:一是定义一种delegate委托类型,事件订阅者只能按照这种类型定义事件的处理方法;二是定义与这个委托相关的event对象,使得订阅者只负责注册和撤销自己的处理过程而不能随意对别人的处理过程产生影响。
从运行结果和reader2对象把同一个处理方法注册了两次的前提可以看到,对于一个事件,同一个订阅者可以把同一个处理过程注册多次,而这个方法最终也会被执行多次。
执行事件订阅列表中方法的顺序不能被保证;而且,在这里采用的是同步调用方法,只有一个响应函数执行完毕,其它函数才会被执行。如果要方法不被阻塞(包括这里的等待用户输入等),就需要采用异步调用方式。