Event Programming in C++ (Part I)
Q:微软的.NET框架让我们能够为托管类定义事件并通过代理和”+=”操作符对其进行处理.那么在本地C++中有没有同样的方法呢,它看起来很有用.
某些读者
A:事实上确实有! Visual C++® .NET有种称为”统一事件模型”的东西能让你用和托管类同样的途径实现本地事件(通过__event关键字),但是本地事件有些微软都没计划要修正的隐晦的技术问题,因此他们让我正式的去阻止你们用它.这是不是意味着C++程序员们只有生活在没有事件的世界中呢?当然不是!不止一种方法能剥猫皮(血腥!).我将告诉你们怎样有条不紊的实现自己华丽的事件系统.
在此之前,我先大概说下关于事件和事件编程的东西,这很重要!在现在这个年月你不能对事件的理解没有一个坚实的基础就来编写代码--它们是什么?什么时候该用它们?
成功的设计全是针对降低复杂性的.很久以前,当函数还被称作”子程序”(我本人为证,我确信),降低复杂性的主要途径就是自上而下的编程.你从一个高层次的目的入手如”为宇宙建模”,然后把它分解成较小的任务如”为银河系建模”和”为太阳系建模”,然后... 直到这些任务变得简单到能在一个函数里实现.自上而下的设计仍然被运用在在程序设计中,但是当系统要对发生顺序不确定的实时事件作出响应时,它就不能很好的工作了.一个经典的例子就是必须对用户操作如点击一个按键或移动鼠标作出响应的GUI程序.事实上, 图形用户接口的出现很大程度上刺激了事件编程的发展.
在自上而下的模式中,处在顶层的高层次模块通过调用像DoThis,DoThat的函数来驱使低层次模块来完成不同的任务.但是低层次模块迟早需要向上回馈.在Windows中,你可以让矩形或椭圆形画出它自己,但最终Windows需要调用你的应用程序来显示它的窗口.但是你的应用程序甚至还根本不存在,它还在计划中!那么Windows怎样才能知道该去调用哪个函数呢?这里就是事件的用武之地.
图 1 自上而下 vs 自下而上
所有基于Windows的应用程序-不管是直接用C编写,还是用封装在MFC或者.NET框架中的类-的核心就是一个处理诸如WM_PAINT,WM_SETFOCUS这样的消息的窗口过程.你(或者说是MFC或.NET)实现了这个窗口过程并且把它传递给Windows操作系统.当需要描绘,改变焦点或者激活窗口时,Windows用适当的消息代码通知你的程序.这些消息就是事件.你的窗口过程就是事件处理者.
如果说过程编程是自上而下,那事件编程就是自下而上.在一个典型的软件系统中,函数调用是从较高层模块流向较底层模块;然而事件以相反的方向流动.图1说明了这个模式.当然,我们的现实世界并不总是这么层次分明的.
很多软件系统看起来更像图2所述.
图 2 混合模式
那么严格来讲到底什么是事件呢?本质上来说它是一个回调.模块用调用你在运行时提供的函数这条途径取代了了调用一个函数名称在编译时究已知的函数.在Windows中,它就叫窗口过程.在.NET框架中它就叫委托.不论术语怎么讲,事件为软件模块提供了一条调用直到运行时才知道的函数的途径.回调就是事件处理程序.激发一个事件就意味着会调用事件处理程序.第一个接收者交给发送者一个注册事件处理程序的指针.
下面是些我们通常会用到事件的一些情形.
向客户端通报实时事件:用户按下一个按键;时间到了午夜;风扇停止,CPU着火了.
报告一个时间很长的操作的进展情况:当拷贝文件或者搜索一个海量数据库时,组件可能会定期的唤起一个事件来报告已经拷贝了多少文件或者已经搜索了多少条记录.
报告一些重要的或者感兴趣的事情的发生:如果你在你的程序中用IWebBrowser2访问Microsoft Internet Explorer,在导航到一个新页面之前和之后或者当它创建了一个新窗口等等它都会通报你.
调用库函数:C运行时库函数 qsort可以对对象数组排序,但是你必须将要比较的对象提供给这个函数.很多STL容器都有这些技巧.大多数程序员不会说qsort回调了一个事件,但是没理由你不能这么想.它是”time to compare”事件.
一些读者偶尔会问:异常和事件的区别是什么?最主要的区别是异常描述的是假定不会发生却发生了的意想不到的情形.比如你的程序用光了内存或用零做了除数.哦,这就是你不希望发生的异常的情形,一旦它们发生了,你的程序必须对它进行处理.然而事件是正常的日常操作的一部分并且完全是意料之中的.用户移动了鼠标或按下一个按键.浏览器导航到新页面.从控制流的角度来看,事件就是一个函数调用,而异常是跨越调用栈的长跳转.
对于事件最普遍的误解是:它们是异步的.尽管事件经常被用来处理用户输入及其他异步操作,事件本身是同步发生的.激活一个事件和调用事件处理是一回事.它看起来像下面这段伪码:
// raise Foo event
for (/* each registered object */) {
obj->FooHandler(/* args */);
}
控制权立即被交给事件处理程序,直到它处理完成才会返回.有些系统提供了异步方式激活事件的途径;如Windows让你用PostMessage替代SendMessage.控制权立即从PostMessage回收,而消息稍后才会被处理.
但是.NET框架事件和我在此讨论的事件都是一旦被激活就立刻被处理.当然,你总是能够从运行在单独的线程的代码里激活事件,或者用异步委托调用来执行导致事件异步(相对于主线程)发生的线程池里的每一个事件处理程序.
Windows处理事件的方式(拥有大量窗口过程和类型一致的WPARAM/LPARAM参数),以现代程序设计的标准来看也是相当简单的.因此所有的Windows程序都在用这一套机制,即使是今天.有些程序员甚至创建不可视窗口来传递事件.窗口过程并不是真实的事件机制,原因在于Windows只允许每个窗口只能有一个窗口过程,因而如果每个窗口过程都调用它之前的,则多个窗口过程就能链接起来.这种流程就是子过程.在真实的事件系统中,可以不分层次的为同样的事件注册不只一个的接收者.
在.NET框架中,事件机制很健全.任何对象都能定义事件,并且多个对象可以监听它们.在.NET中,事件通过委托(.NET中回调的替代词)来工作.更重要的是委托是类型安全的,不再有void*或WPARAM/LPARAM之类的东西.
在托管扩展中,要定义一个事件可以用__event关键字.例如Windows::Forms中的Button类有一个Click事件:
// in Button class
public:
__event EventHandler* Click;
事件处理程序是一个接受一个Object和EventArgs参数的函数委托:
public __delegate void EventHandler(
Object* sender,
EventArgs* e
);
要接收事件你要实现一个有正确的参数的处理者成员函数并创建一个委托封装它,然后调用事件操作符”+=”来注册你的处理函数/委托.对于Click事件,它看起来像这样:
// event handler
void CMyForm::OnAbort(Object* sender, EventArgs *e)
{
...
}
// register my handler
m_abortButton->Click += new EventHandler(this, OnAbort);
注意处理函数必须和委托定义的参数一样.所有这些都能在MSDN的Managed Extensions 101找到.但是你问的并不是托管事件而是本地事件-怎样在本地C++中实现事件?C++没有内建的事件机制,你能做什么呢?你能用typedef来定义一个回调函数然后让客户接受它,有点像qsort那顶旧帽子.更不毕说当你要处理几个事件时会有多麻烦.当你想用成员函数取代静态外部函数来作为事件处理函数,它会尤其丑陋!
定义事件更好的方法是创建一个接口.COM就是这么做的.但是你不毕用C++实现所有的COM代码,你可以用一个简单的类.我以自己写的一个类名为CPrimerCalculator的类为例,它用来找出的质数.当它运行时,它会激活两种类型的事件:一个”进度”事件和一个”完成”事件.这些事件由IPrimeEvents接口定义.从.NET或COM的角度来看IPrimeEvents就是一个接口;它就是一个普通而古老的定义了签名(参数和返回类型)的C++抽象类.所有处理CPrimerCalculator事件的客户都必须实现IPrimeEvents,然后调用CPrimeCalculator::Register注册它们的接口.CPrimeCalculator将这些对象/接口添加到自身的链表中去.当CPrimeCalculator测试每个整数的质数性时,它会周期性的报告截止到目前一共找到了多少质数:
// in CPrimeCalculator::FindPrimes
for (UINT p=2; p<max; p++) {
// figure out if p is prime
if (/* every now and then */)
NotifyProgress(GetNumberOfPrimes());
...
}
NotifyDone();
CPrimeCalculator调用自己的助手函数NotifyProgress和NotifyDone来激活事件.这些函数遍历事件链表,调用每个客户的合适的事件处理函数.代码如下:
void CPrimeCalculator::NotifyProgress(UINT nFound)
{
list<IPrimeEvents*>::iterator it;
for (it=m_clients.begin(); it!=m_clients.end(); it++) {
(*it)->OnProgress(nFound);
}
}
如果你还没忘记STL,你会明白迭代器的解除引用操作符会返回当前对象,上面代码中的for循环里的那一行等同于下面:
IPrimeEvents* obj = *it;
obj->OnProgress(nFound);
有一个类似的不带参数的NotifyDone函数能激发完成事件.你应该明白在客户知道当FindPrimes返回控制权时CPrimeCalculator已经完成之前没有必要用”完成”事件.除了一种情况以外你可能是对的.那就是可能不止有一个客户为接收事件注册了,并且很可能不是同一个客户调用了CPrimeCalculator::FindPrimes.图3就是我的PrimeCalc测试程序.PrimeCalc为质数事件实现了两种不同的事件处理过程.第一个就是主对话框本身CMyDlg,它利用多重继承实现了IPrimeEvents接口.这个对话框处理了OnProgress和OnDone事件,它在窗口中显示进度并且当完成是有提示音.另一个是CTracePrimeEvents,它同样实现了IPrimeEvents接口.它的实现是显示诊断信息.写CTracePrimeEvents的目的是为了展示怎样为同样的事件注册一个以上的客户.
图 3 PrimeCalc in Action
在用CPrimeCalculator写应用程序的程序员看来处理事件是很简单而清晰的.从IPrimeEvents派生,实现处理函数,然后注册自己.在通过写各种类来激发事件的程序员看来,这个过程是有点单调乏味的.首先你得定义事件接口,这还凑活.但紧接着你的写注册和解除注册函数,更不毕说为每一个Foo事件写下NotifyFoo函数.如果你有15个事件,特别是每一个NotifyFoo函数都像一个娘胎里出来似的,这就相当讨厌了.
void CMyClass::NotifyFoo(/* args */)
{
list<IPrimeEvents*>::iterator it;
for (it=m_clients.begin(); it!=m_clients.end(); it++) {
(*it)->OnFoo(/* args */);
}
}
图 4 PrimeCalc在TraceWin中的输出
迭代客户链表中的NotifyFoo,为每一个已注册客户调用适当的OnFoo处理函数,传给它需要的任何参数.有没有什么方法能用宏或模板或其它什么东西把它通用化来减轻这份苦差事把你从写一些令人厌烦的样板代码中解放出来呢?事实上,确实有!在下个月我会向你们展示.同样的时间,同样的频道.在此之前-Happy Programming!
posted on 2006-03-16 11:08
Dr.Magic 阅读(1118)
评论(2) 编辑 收藏 引用 所属分类:
译海浅涉