实现C++内部事件机制
Q The Microsoft® .NET Framework lets me define events for managed classes and handle them using delegates and operator+=. Is there some way to do the same thing in native C++? It seems like this would be useful.
Several Readers
问:Microsoft .NET Framework 允许我在类中定义事件成员并用代理和操作符+=操纵它们。是不是有办法在C++内部实现这些功能呢?它看起来很有用呀。
A Indeed it would be! Visual C++® .NET has something called the Unified Event Model that lets you implement native events the same way you would for a managed class (using the __event keyword), but alas native events have some obscure technical problems which the Redmondtonians don't plan to fix, so they've asked me to officially discourage you from using them. Does that mean C++ programmers have to live without events? Of course not! There's more than one way to skin a cat. I'll show you how to implement your own nifty event system with very little fuss.
答:的确是这样的!Virtual C++ .NET 存在某种调用统一事件模型的机制,这个模型允许你实现类的内部事件(使用关键字__event),也就是你所提到的方法。但是,另人难过的是内部事件存在一些晦涩的技术难题,而且 the Redmondtonaians(指微软公司) 也没有计划去解决它们,因此他们明确要求我阻止您使用内部事件。这是否意味着C++程序员们只能生活在没有事件的窘境中呢?当然不是!条条大路通罗马。我将向您展示一下如何用极少的代价来做个漂亮的属于你自己的事件系统。
But before I do, let me write some words about events and event programming in general. It's an important topic. You can't program in the modern world without a solid understanding of events—what they are and when to use them.
但是在开始实现之前,让我先就事件和事件编程唠叨几句。这可是个重要的课题。对事件透彻的理解是现代编程的基础,包括事件是什么以及何时使用他们。
Successful programming is all about managing complexity. A long time ago, when functions were called "subroutines" (I'm dating myself, I know), one of the main ways to manage complexity was top-down programming. You start with some high-level goal like "model the universe," then break it into smaller tasks like "model the galaxies" and "model the solar system" and so on until the tasks become small enough to implement in a single function. Top-down programming still works for procedural tasks, but it doesn't work well for systems that respond to real-world events that happen in a nondeterministic order. The classic example is a GUI where your program must respond when the user does something like press a key or move the mouse. Indeed, event programming was largely spurred by the advent of graphical user interfaces.
程序的成功之处在于管理纷繁复杂的事物。很久以前,函数被称为子程序(我知道,我有点摆老资格),管理复杂事物的主要方法之一是从上至下的程序设计。你从一些很高层的目标开始,比如模拟宇宙,然后把它分解成较小的任务,就象模拟银河系和太阳系等等,直到这些任务被分解成在一个函数里就能够实现为止。从上至下的编程技术比较适用于有固定流程的任务,而对于响应真实世界中无序事件的系统,它就有点力不从心了。GUI就是个典型的例子,在GUI中你的程序必须对用户按键或移动鼠标做出反应。事实上,在很大程度上正是图形用户界面推动了事件编程技术的发展。
In the top-down model, high-level components at the top of the food chain bark orders to lower-level components by calling functions like DoThis and DoThat to perform various tasks. But sooner or later, the lower-level components need to talk back. In Windows®, you can call Rectangle or Ellipse to draw a rectangle or ellipse, but eventually Windows needs to call your app to paint its window. But your app doesn't even exist yet, it's still behind schedule! So how can Windows know which function to call? This is where events come in.
在从上至下模式中,食物链上层的组件通过调用这样那样的函数来对下层组件发布命令。但是迟早,低层组件总要回调。在Windows 操作系统中,你可以调用Rectange和 Ellipse来画一个矩形或椭圆,但最终操作系统需要回调你的应用来绘制它的窗口。但是你的应用甚至还不存在,它还没有被列上计划勒!因此操作系统怎么知道调用哪个函数呢?这正是事件的用武之地。
Figure 1 Top-Down vs. Bottom-Up
At the core of every Windows-based program—whether directly coded in C, or hidden behind layers of MFC or .NET Framework classes—is a window procedure that processes messages like WM_PAINT, WM_SETFOCUS, and WM_ACTIVATE. You (or MFC or .NET) implement the window procedure and pass it to Windows. When it's time to paint, change the focus, or activate your window, Windows calls your procedure with the appropriate message code. The messages are the events. Your window procedure is the event handler.
对于每个基于Windows操作系统的程序,不管它是直接的C编码,还是被隐藏在MFC或NET框架类后面。它都是一个处理象WM_PAINT,WM_SETFOCUS,和WM_ACTIVATIVE这样的消息的窗口程序。你(或MFC或NET)来实现窗口程序并把它传给操作系统。当到了描绘的时间,或焦点发生了改变,或你的窗口被激活,操作系统会调用你的程序中与消息代码相适配的部分。这里的消息就是事件,你的窗口程序就是事件处理者。(注:窗口只是展现在你面前的一个木偶,所有具体的操作都是由后面的应用来完成,事件恰似两者之间的纽带)
If procedural programming is top-down, event programming is bottom-up. In a typical software system, function calls flow downward from higher-level components to lower-level ones; whereas events percolate up in the opposite direction. Figure 1 illustrates this pattern. Of course, the real world in which we romp is not always so neatly hierarchical. Many software systems look more like Figure 2.
如果基于固定流程的编程是从上至下,那么基于事件的编程就是从下至上的。在一个典型的软件系统中,功能调用从上层组件流向下层组件;然而事件以相反的方向传上去。图1说明了这个模式。当然,我们生活的现实世界并不总是如此层次分明。许多软件系统看起来更象图2所表示的那样。
Figure 2 A Mixed Model
So what exactly is an event? Essentially, it's a callback. Instead of calling a function whose name is known at compile time, the component calls a function you provide at run time. In Windows, it's the window proc. In the .NET Framework, it's a delegate. Whatever the terminology, events provide a way for one software component to call another without knowing until runtime what function to call. The callback is called the event handler. Raising or firing an event means calling the handler. To get the ball rolling, the receiving component first gives the source component a pointer to its event handler, a process called registration.
那么事件到底是什么东西呢?本质上,它就是一个回调。代替调用一个编译期已经确定的功能,组件调用一个你在运行期提供的功能。在Windows操作系统中,它是窗口过程。在NET框架中,它是一个代理。无论术语如何变化,事件为一个组件调用另一个直到运行期才能确定的功能提供了一条途径。 回调被称为事件处理者。触发一个事件意味着调用这个处理者。为了使二者联系起来,接收事件的组件首先传递一个指向事件处理者的指针给触发事件的组件,这个过程称为注册。
Here are some common situations in which events are used.
下面是应用事件的几种常见情况。
To Notify Clients About Real-World Events The user pressed a key; the clock struck midnight; the fan failed and the CPU is on fire.
向客户端通报实时事件: 使用者按下一个键;时钟半夜罢工;风扇停止导致CPU过热。
To Report Progress of Lengthy Operations When copying files or searching a huge database, a component might periodically raise an event to report how many files have been copied or how many records have been searched.
报告长操作的进展 : 当复制多个文件或搜索一个大型数据库时,组件可能定时触发一个事件来报告已经复制的文件数或已经搜索到的记录数。
To Report When Something Significant or Potentially Interesting Has Happened If you use IWebBrowser2 to host Microsoft Internet Explorer in your app, the browser will notify you before and after navigating to a new page, or when it creates a new window—among other things.
通告重大事件或潜在的感兴趣的问题的发生: 如果你在应用中使用 IwebBrowswe2 来host 微软互联网页,在导航到一个新网页前后浏览器会通告你,或者它创建一个新窗口。
To Invoke an Application-Supplied Algorithm The C runtime library function qsort sorts an array of objects, but you must supply the function that compares objects. Many STL containers let you play the same trick. Most programmers wouldn't call the qsort callback an event, but there's no reason you can't think of it that way. It's the "time to compare" event.
C运行库的(qsort)为对象数组提供排序功能,但是你必须提供对象比较规则。许多STL容器都允许你玩弄同样的把戏。大多数程序员不会称这个qsort回调了一个事件,但你完全有理由这样认为。它就是个定时对比事件。
Some readers occasionally ask: What's the difference between an exception and an event? The main difference is that exceptions represent unexpected conditions that aren't supposed to occur. For example, your program runs out of memory or encounters divide-by-zero. Oops. These are aberrant situations you hope won't happen, and yet if they do your program has to cope. Events, on the other hand, are part of normal everyday operation and are fully expected. The user moves the mouse or presses a key. The browser just navigated to a new page. From a control-flow perspective, an event is a function call, whereas an exception is a long jump across the stack, with unwinding semantics to destroy lost objects.
有些读者偶尔会问:异常和事件之间有什么存在什么区别呢?主要的区别就是异常描述的是不希望发生的情况。例如,你的程序耗尽了内存或遇到零做除数。哎,这些都是你不愿见到的呀,然而如果它们发生了,你的程序就得处理。换句话说,事件是日常行为的一部分,完全在意料之中。用户移动鼠标或按下一个键。浏览器刚刚导航到一个新页面。从控制流的角度来看,事件是功能调用,而异常则是越过堆栈的长跳转,利用展开语义来摧毁丢失的对象。(注:即栈展开,随着栈的展开,在退出的复合语句和函数定义中声明的局部变量的生命期也结束了,而且C++保证,尽管局部类对象的生命期是因为抛出异常而结束,但这些局部对象的析构函数也会被调用)
A common misconception about events is that they're asynchronous. While events are often used to handle user input and other actions that happen asynchronously, the events themselves happen in synchronous fashion. Raising an event is the same thing as calling the event handler(s). In pseudocode, it looks something like the following snippet:
一个对于事件常见的误解是认为它们是异步的。尽管事件常常被用于管理用户输入和其他异步发生的动作,但事件本身则是以同步方式发生的。触发一个事件和调用这个事件的处理者是同一码事。下面的片断是其伪码表示:
// raise Foo event
for (/* each registered object */) {
obj->FooHandler(/* args */);
}
Control passes immediately to the event handler and doesn't return until it's done executing. Some systems provide a way to raise events asynchronously; for example, Windows lets you use PostMessage instead of SendMessage. Control returns immediately from PostMessage and the message gets processed later. But .NET Framework events and the events I'll discuss here are handled immediately when you raise them. Of course, you can always raise an event from code that runs in a separate thread, or you can use asynchronous delegate invocation to execute each event handler in the thread pool, in which case the event happens asynchronously relative to the main thread.
控制立即被传递到事件处理器并且直到其执行结束才返回。某些系统提供了异步触发事件的机制;例如,Windows 操作系统允许你用PostMessage 代替SendMessage。控制立即从PostMessage调用返回,而消息留待以后处理。(注:我能想到的一个实现机制是把消息存进了一个消息队列) 但是NET框架的事件以及我在这里谈到的事件都是在你触发时立即处理的。当然,在一个独立运行的线程里触发事件是可取的,或者你可以利用异步代理调用来执行线程池中的事件管理者,在这种情况下事件相对于主线程来说是异步发生的。
The way Windows does events, with its omniversal window proc and type-deadly WPARAM/LPARAM parameters, is downright primitive by modern programming standards. Yet every Windows program uses this mechanism, even today. Some programmers even create invisible windows just to pass events. The window proc is not a true event mechanism because Windows allows only one window proc per window, though multiple procs can be linked if each proc calls the one before it. This process is known as subclassing. In a true event system, more than one recipient can register for the same events in a non-hierarchical way.
Windows操作系统处理事件的方法,用它的整个窗口过程和类型固定的WPARAM/LPARAM参数,相对于现代编程标准来说是相当粗糙。然而即使到今天所有Windows程序都使用这个机制。仅仅为了传递事件,有些程序员甚至不惜创建无形的窗口。窗口过程并非真正的事件机制,因为Windows操作系统仅仅允许每个窗口对应一个窗口过程,虽然多个过程可以通过串联的调用联结起来。这个处理被称为subclassing.在真实的事件系统中,多个接受者能以平等的方式注册到同一个事件上。
In the .NET Framework, events are full-fledged citizens. Any object can define events, and multiple objects can listen to them. In .NET, events work using delegates, which is Framework terminology for a callback. Most important, delegates are type-safe. No more void*'s or WPARAM/LPARAM.
在NET框架中,事件无处不在。任何对象都能定义多个事件,而且可以有多个对象监听它们。在NET框架中,事件利用代理来工作,它是针对回调的另一个说法。更重要的是,代理是类型安全的。不在有 void * 或WPARAM/LPARAM参数。
To define an event using the Managed Extensions, you use the __event keyword. For example, the Button class in Windows::Forms has a Click event:
为了利用这个被管理的功能来定义一个事件,使用__event关键字。例如,Windows::Forms 中的Button类拥有Click事件:
// in Button class
public:
__event EventHandler* Click;
EventHandler is a delegate for a function that takes an Object (the sender) and EventArgs:
EventHandler 是一个函数的代理,它带有一个Object(事件的发送者)和EventArgs:
public __delegate void EventHandler(
Object* sender,
EventArgs* e
);
To receive events, you implement a handler member function with the right signature and create a delegate to wrap it, then invoke the event's operator+= to register your handler/delegate. For the Click event, it looks like this:
为了接收事件,你得实现一个带有正确符号特征signature的处理成员函数并创建一个代理来包装它。然后调用事件的 +=操作符注册你的处理者/代理。对于Click事件,看起来象这样:
// event handler
void CMyForm::OnAbort(Object* sender, EventArgs *e)
{
...
}
// register my handler
m_abortButton->Click += new EventHandler(this, OnAbort);
Note that the handler function must have the signature defined by the delegate. All of this is Managed Extensions 101. But you didn't ask about managed events, you asked about native events—how can you implement events in native C++? C++ has no built-in event mechanism, so what can you do? You could use a typedef to define a callback and make clients supply it, something similar to qsort—but that's so old-hat. Not to mention cumbersome if you are working with several events. And it's especially ugly if you want to use member functions as the event handlers, as opposed to static extern functions.
注意处理函数必须有代理定义的符号特征(signature)。All of this is Managed Extensions 101. (注:鄙人愚钝,左思右想不得其译,望高人教我)但你并不是问管理的事件,你要求的是纯粹的事件,如何才能在纯粹C++下实现事件呢?C++并没有内建的事件机制,那么你能做些什么呢?似乎你可以用 typedef 来定义一个回调,并让用户支持它,就象qsort这样的东东,但它已经过时了,更不必说你同时处理多个事件就会有麻烦。而且相对与static extern 函数,用成员函数作为事件处理者就显得非常别扭。
A better way is to create an interface that defines the events. That's how COM sdoes it. But you don't need all the overhead of COM code in C++; you can use a simple class. I wrote a class called CPrimeCalculator that shows how; it finds prime numbers. Figure 3 shows the code. CPrimeCalculator::FindPrimes(n) finds the first n prime numbers. While it's working, CPrimeCalculator can raise two kinds of events: a Progress event and a Done event. The events are defined in an interface IPrimeEvents. IPrimeEvents isn't an interface in the .NET or COM sense; it's a plain old C++ abstract base class that defines the signature (parameters and return type) for each event handler. Clients that handle CPrimeCalculator events must implement IPrimeEvents, then call CPrimeCalculator::Register to register their interface. CPrimeCalculator adds the object/interface to its internal list. As it tests every integer for primeness, CPrimeCalculator periodically reports how many primes it has found so far:
一个较好的方法是创建一个定义事件的接口。COM就是那样做的。但你不需要象COM那样费事;你可以用一个比较简单的类。我写了个叫做CprimeCalculate的类来演示该如何做;它找到一组素数。图3显示它的代码。CprimeCalculator::FindPrimes(n)找到第一个包括N个素数的组。当它工作的时候,Cprimecalculator 能触发两种事件;Progress事件和 Done事件。这些事件在接口IprimeEvents中定义。IprimeEvents并不是.NET 和COM意义上的接口;它只是一个普通的C++抽象基类,它为每个事件处理者定义了符号特征signature(参数表和返回值)。处理CprimeCalculator事件的客户必须实现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 calls internal helper functions NotifyProgress and NotifyDone to raise the events. These functions iterate the client list, calling the appropriate event handler for each client. In code, it looks like this:
CprimeCalcultor 调用内部辅助函数NotifyProgress和 NotifyDone来触发事件。这些函数迭代客户列表,调用合适的事件处理器。下面的代码给出了大概流程。
void CPrimeCalculator::NotifyProgress(UINT nFound)
{
list<IPrimeEvents*>::iterator it;
for (it=m_clients.begin(); it!=m_clients.end(); it++) {
(*it)->OnProgress(nFound);
}
}
In case you're not up on your STL, recall that for iterators the dereference operator returns the current object, so the line inside the for loop in the preceding snippet is equivalent to:
万一你对STL并不十分精通,对适配器调用解除引用操作符返回当前对象,因此在上面代码片段for 循环中与下面的代码相同。
IPrimeEvents* obj = *it;
obj->OnProgress(nFound);
There's a similar function NotifyDone that raises the Done event, which has no parameters. Figure 3 shows the code. You might think there's no need for a Done event since the client knows CPrimeCalculator is done when FindPrimes returns control. And you'd be right—except for one thing. There may be more than one client registered to receive events, and possibly not the same one that calls CPrimeCalculator::FindPrimes. Figure 4 shows my test program, PrimeCalc. PrimeCalc implements two different event handlers for prime events. The first handler is the main dialog itself, CMyDlg, which implements IPrimeEvents using multiple inheritance. The dialog handles OnProgress and OnDone by displaying the progress in a dialog window (see Figure 5) and beeping when done. The other event handler, CTracePrimeEvents, also implements IPrimeEvents. Its implementation displays information in the diagnostic (TRACE) stream. Figure 6 shows a sample run in my TraceWin applet. (See my March 2004 column, or download TraceWin from http://www.dilascia.com/.) I wrote CTracePrimeEvents to show how more than one client can register for the same events.
这儿有一个相似的函数NotifyDone,它触发Done事件,它不需要参数。图3显示其代码。你可能认为Done事件没有必要,因为当FindPrimes返回控制时客户就知道CprimeCalculator 被执行了。你是正确的,除了一件事之外。可能不止一个用户注册接收事件,并且调用Cprimecalculator::FindPrimes的并非同一个。图4显示了我的检测程序,PrimeCalc为素数事件实现了两个不同的事件处理者。第一个是主对话本身,CMyDlg,它利用多重继承来实现IprimeEvents.对话在对话窗口显示处理过程并伴随嘟嘟声来响应OnProgress和OnDone。另外一个事件处理者,CTracePrimeRvents,同样实现了IprimeEvents,它以诊断流的方式显示信息。 图6显示是一个运行样本。(见我的 2004年3月专栏,或从http://www.dilascia.com下载TraceWin)我写了CtracePrimeEvents 来演示多个用户是如何注册同一个事件的。
Figure 5 PrimeCalc in Action
From the perspective of a programmer writing an application that uses CPrimeCalculator, handling events is simple and straightforward. Derive from IPrimeEvents, implement the handler functions, then call Register. For programmers writing classes that raise events, the process is a bit more tedious. First you have to define the event interface. That's not bad. But then you have to write Register and Unregister functions, not to mention a NotifyFoo function for each Foo event. Quite a bore if you have 15 events, especially since each NotifyFoo function has the same pattern:
站在一个程序员的角度,一旦使用CprimeCalculator来编程,处理事件就变的简单和直截了当。继承IprimeEvents,实现处理函数,然后调用注册。而如果通过写类来触发事件,这个过程要麻烦的多。首先你不得不定义事件接口。这还不算太糟,但是你还要写注册和注销函数,更不用说针对每个事件的NotifyFoo函数。如果你有15个事件,它就会变的非常讨厌,特别是每个NotifyFoo都是一个模子刻出来的。
void CMyClass::NotifyFoo(/* args */)
{
list<IPrimeEvents*>::iterator it;
for (it=m_clients.begin(); it!=m_clients.end(); it++) {
(*it)->OnFoo(/* args */);
}
}
Figure 6 PrimeCalc Output in TraceWin
NotifyFoo iterates the client list, calling the appropriate OnFoo handler for each registered client and passing whatever parameters are required. Isn't there some way to implement this generically using macros or templates or something, in order to encapsulate the drudgery and spare yourself the misery of writing such boring boilerplate code? In fact, there is. I'll show you how next month. Same bat time. Same bat channel. Until then—Happy programming!
NotifyFoo 迭代用户列表,针对每个注册用户调用合适的OnFoo并传递需要的参数。难道没有通用点的方法来实现这些吗?比如用宏或模板什么的来封装这个苦差使,还可以减少重复写同样代码的苦恼。事实上,确实存在一个,下个月我将把它展示在您面前。同一个时间,同样一个频道。在那个时刻到来之前,让我们快乐编程吧!
发送你的问题到cppqa@microsoft.com
Send your questions and comments for Paul to cppqa@microsoft.com.
Paul DiLascia is a freelance software consultant and Web/UI designer-at-large. He is the author of Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992). In his spare time, Paul develops PixieLib, an MFC class library available from his Web site, http://www.dilascia.com/.
最后,请允许我介绍一下本文的作者 Paul DiLascia。一个自由软件咨询师和Web/UI 设计者。他是Windows++的作者;在业余时间里,保罗开发了PixieLib,一个MFC类库,可以在他的网站上找到它,http://www.dilascia.com/.
鄙人msn联系方式:wangdongyu3d@hotmail.com (常在线)