.NET中的委托—事件机制: 办公室的故事
Chris Sells 著 ( <<ATL Internals>>一书作者之一,该书是ATL编程的宝典)
Jackeygou 译 研发中心
软件技术的动人美感来源于对现实世界的真实理解. 一— 译注
===========================================
强类型耦合
------------
从前在我们这个城市的西南角,有一家小技术服务公司,公司里有一位聪明能干的年轻人,他的名字叫Peter。不幸的是他的老板却是一位吝啬、多疑,而且极为循规蹈矩的小人,例如下属的任何工作都必须先报告,而且经他审批后才能进行。可怜的Peter自然不愿他的老板整日里站在自己的身后虎视眈眈,于是他对老板保证,自己的任何工作进度都会向他及时通禀。Peter实现这一承诺的方法就是周期性的利用类型引用回调boss,把他老板叫过来审查。程序实现如下:
class Worker {
public void Advise(Boss boss) { _boss = boss; }
public void DoWork() {
Console.WriteLine("Worker: work started");
if( _boss != null ) _boss.WorkStarted(); // 开始工作的审批
Console.WriteLine("Worker: work progressing");
if( _boss != null ) _boss.WorkProgressing(); // 进行工作的审批
Console.WriteLine("Worker: work completed");
if( _boss != null ) {
int grade = _boss.WorkCompleted(); // 完成工作的审批
Console.WriteLine("Worker grade= " + grade);
}
}
private Boss _boss;
}
class Boss {
public void WorkStarted() { /* 老板实际上并不很关心. */ }
public void WorkProgressing() { /*老板实际上并不很关心. */ }
public int WorkCompleted() {
Console.WriteLine("It's about time!");
return 2; /* 满分10分,才给2分,够吝啬小气吧. */
}
}
class Universe {
static void Main() {
Worker peter = new Worker(); // 生成peter实例
Boss boss = new Boss(); // 生成boss实例
peter.Advise(boss);
peter.DoWork();
Console.WriteLine("Main: 工作结束!");
Console.ReadLine();
}
}
【译注:以下是上段程序输出结果:
Worker: work started
Worker: work progressing
Worker: work completed
It's about time!
Worker grade = 2
Main: worker completed work
】
接口
----------
现在Peter已经成为一个特殊的成员,因为它不仅要忍受它那位吝啬老板的指使,而且还与Universe对象紧密相关(没办法谁让他身不逢时处于Universe类的Main函数中)。这种“亲密接触”使Peter觉得Universe对他的工作进度也很感兴趣。不幸的是,除了保证boss能够被通知外,如果不为Universe添加一个特殊的通知方法和回调,Peter无法向Universe通知其工作进度。而Peter首先要做的就是从具体的通知执行过程中分离出隐藏的通知约定,而这就需要定义接口了:
interface IWorkerEvents {
void WorkStarted();
void WorkProgressing();
int WorkCompleted();
}
class Worker {
public void Advise(IWorkerEvents events) { _events = events; }
public void DoWork() {
Console.WriteLine("Worker: work started");
if( _events != null ) _events.WorkStarted();
Console.WriteLine("Worker: work progressing");
if(_events != null ) _events.WorkProgressing();
Console.WriteLine("Worker: work completed");
if(_events != null ) {
int grade = _events.WorkCompleted();
Console.WriteLine("Worker grade= " + grade);
}
}
private IWorkerEvents _events;
}
class Boss : IWorkerEvents {
public void WorkStarted() { /* boss doesn't care. */ }
public void WorkProgressing() { /* boss doesn't care. */ }
public int WorkCompleted() {
Console.WriteLine("It's about time!");
return 3; /* out of 10 */
}
}
【译注:以下是上段程序输出结果:
Worker: work started
Worker: work progressing
Worker: work completed
It's about time!
Worker grade = 3
Main: worker completed work
】
委托
-----------
不幸的是,Peter整日忙于通知他的老板去负责执行接口,而无暇顾及通知Universe。但是Peter觉得这已经不远了,因为至少他已经成功的将对具体老板的引用,抽象为对统一的IWorkerEvents接口的引用了。换句话说,别的实现了IWorkerEvents接口的什么人都可以收到工作进度通知。
就这样,过了一段时间,Peter的老板依然对Peter很不满,他大声的抱怨到:“Hi! Peter,你知道不知道,你真的很烦呀!为什么你每次在开始工作和工作进行时都要叫上我,我并不是每次都对这些过程感兴趣。你不仅强迫我做一些我不感兴趣的事情,而且你还浪费了大量宝贵的工作时间在等我从审批事件中返回。如果我很忙,不能做出回答,难道你就要放假不成?你能不能够别这样来打搅我,OK?”
此时,Peter逐渐意识到采用接口在很多时候时是非常有用的(译注:接口在COM世界里,可以称得上是万物之本),但是用接口来处理事件时,就有些粒度(译注:不是力度)不够精细。他希望在事件发生时只通知哪些对该事件感兴趣的人,而不是让一个人必须对所有的事件感兴趣。所以,Peter将接口进一步肢解成更小的独立的委托函数,每一个委托函数可以看作是一个轻量级的函数接口,意义等价于类接口。
delegate void WorkStarted();
delegate void WorkProgressing();
delegate int WorkCompleted();
class Worker {
public void DoWork() {
Console.WriteLine("Worker: work started");
if( started != null ) started();
Console.WriteLine("Worker: work progressing");
if( progressing != null ) progressing();
Console.WriteLine("Worker: work completed");
if( completed != null ) {
int grade = completed();
Console.WriteLine("Worker grade= " + grade);
}
}
public WorkStarted started;
public WorkProgressing progressing;
public WorkCompleted completed;
}
class Boss {
public int WorkCompleted() {
Console.WriteLine("Better...");
return 4; /* out of 10 */
}
}
class Universe {
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed = new WorkCompleted(boss.WorkCompleted);
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
}
【译注:以下是上段程序输出结果:
Worker: work started
Worker: work progressing
Worker: work completed
Better...
Worker grade = 4
Main: worker completed work
】
【译注:对“但是用接口来处理事件时,就有些粒度不够精细。”的理解可用下例说明,请仔细观察一下程序,如果全部换作接口定义去处理,思考一下这样做的不利之处。欢迎大家讨论:
using System;
interface IWorkStartedEvent
{
void WorkStarted();
}
interface IWorkProgressingEvent
{
void WorkProgressing();
}
interface IWorkCompletedEvent
{
int WorkCompleted();
}
class Worker
{
public void Advise(IWorkCompletedEvent AEvent)
{
_event = AEvent;
}
public void DoWork()
{
Console.WriteLine("Worker: work completed");
if(_event != null )
{
int grade = _event.WorkCompleted();
Console.WriteLine("Worker grade = " + grade);
}
}
private IWorkCompletedEvent _event;
}
class Boss : IWorkCompletedEvent
{
public int WorkCompleted()
{
Console.WriteLine("Better...");
return 4; /* out of 10 */
}
}
class Universe
{
static void Main()
{
Worker peter = new Worker();
Boss boss = new Boss();
peter.Advise(boss);
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
}
以下是上段程序输出结果:
Worker: work completed
Better...
Worker grade = 4
Main: worker completed work
】
静态响应函数
---------------
这样一来,果然立竿见影。Peter再也不会因为开始工作审批等事件而去打搅他的老板。但是现在的问题是Peter依然没有将Universe列入他的事件响应者名单。因为Universe是一个全封闭实体类(all-compassing entity),所以,如果需要与委托相连,就要实例化Universe,实在没必要(设想一下Universe的多个实例需要多少资源…)。取而代之,采用静态函数实现。因为委托支持这些静态函数定义:
class Universe {
static void WorkerStartedWork() {
Console.WriteLine("Universe notices worker starting work");
}
static int WorkerCompletedWork() {
Console.WriteLine("Universe pleased with worker's work");
return 7;
}
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed = new WorkCompleted(boss.WorkCompleted);// #
peter.started = new WorkStarted(Universe.WorkerStartedWork);
peter.completed = new WorkCompleted(Universe.WorkerCompletedWork); // 这一行使得前面#行白费了!boss被Universe取代了,也许Peter更愿意如此。
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
}
【译注:以下是上段程序输出结果:
Worker: work started
Universe notices worker starting work
Worker: work progressing
Worker: work completed
Universe pleased with worker's work
Worker grade = 7
Main: worker completed work
】
事件
----------
事与愿违,虽然Peter很乐意让Universe参与合作,但Universe自身很忙也不习惯将自己的全部身心都投入到单一的Peter实例中,以取代Peter老板委托的位置。此外Peter类中的委托字段是公有访问权限也会存在一些未知的负面作用。例如,那天Peter的老板想不开,突然决定要亲自激活Peter的委托任务,也尚未可知。(按此人的多疑易怒的性情是很有可能的!)
// Peter的老板自己将委托任务激活,这就是公有权限的恶果!!!
if( peter.completed != null ) peter.completed();
可怜的Peter自然不愿意上述的任何一种情况发生,他需要实现对于每一个委托都能加入注册和反注册函数,只有这样事件响应者可以添加和删除自身, 但谁都不能够清空整个事件列表,避免出现刚才Universe将boss取而代之的结果。Peter自身对此是无能为力了,但是通过C#提供的“event”关键字,Peter就可以梦想成真了:
class Worker {
...
public event WorkStarted started;
public event WorkProgressing progressing;
public event WorkCompleted completed;
}
Peter知道利用C#的委托—事件机制可以轻松搞定,而且这时event关键字提供了一个关于委托的属性(property),可以允许C#客户方便地通过“+=”和“-= ”添加和删除他们自定义的执行函数,而不像委托那样挂上就甩不掉:
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed += new WorkCompleted(boss.WorkCompleted);
peter.started += new WorkStarted(Universe.WorkerStartedWork);
peter.completed += new WorkCompleted(Universe.WorkerCompletedWork);
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
【译注:以下是上段程序输出结果
Worker: work started
Universe notices worker starting work
Worker: work progressing
Worker: work completed
Better...// 【译注:boss也通知到啦,刚才打‘#’那代码被执行了。但是且慢,boss打的那4分没有得到,后面只得到了Universe给的7分】
Universe pleased with worker's work
Worker grade = 7
Main: worker completed work
】
查询所有结果
--------------
对于这一点,Peter满怀信心。他可以方便管理所有与事件相关的执行者信息,而不需要与具体的执行者产生紧耦合关系。Peter注意到他的最终工作评定(completed事件)关联了两个事件执行函数:boss和Universe。他们都给Peter一个评定分数4分和7分。正是春风得意的Peter自然不会随意丢掉任何一个结果,他希望能得到每一个响应者的评分结果。因此,他决定提取委托调用列表,以便手工分别调用它们并累加,这样他的Money….
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
int grade = 0;
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
grade += wc();
Console.WriteLine("Worker grade= " + grade);
}
}
}
【译注:以下是上段程序输出结果
Worker: work started
Universe notices worker starting work
Worker: work progressing
Worker: work completed
Better...
Worker grade = 4 【译注:boss打的4分也得到啦】
Universe pleased with worker's work
Worker grade = 11【译注:Peter共得了11分!】
Main: worker completed work
】
异步通知:激活和放弃
------------------------
Peter很快又发现了新的问题,他的老板和Universe有时都会出现正为别的事忙碌而无暇顾及Peter的工作评定,这就使得Peter的工作评定时间被拖延了:
class Boss {
public int WorkCompleted() {
System.Threading.Thread.Sleep(3000); // 延时3秒
Console.WriteLine("Better..."); return 6; /* out of 10 */
}
}
class Universe {
static int WorkerCompletedWork() {
System.Threading.Thread.Sleep(4000); // 延时4秒
Console.WriteLine("Universe is pleased with worker's work");
return 7;
}
...
}
噢!看到了,这确实很容易造成时间的浪费,而且最让Peter恼火的是,周末他与Kristin的约会都被搅黄了。不行,这一定要改正!于是Peter决定放弃工作评定打分而改为异步委托—事件激活:
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() )
{
wc.BeginInvoke(null, null);
}
}
}
【译注:以下是上段程序输出结果
Worker: work started
Universe notices worker starting work
Worker: work progressing
Worker: work completed
Main: worker completed work //【译注:由于是异步触发事件,因此这一行先输出啦】
Better... //【译注:评分已被忽略】
Universe pleased with worker's work //【译注:评分已被忽略】
】
异步通知:缓存池(Polling)
-------------------------------
这就使得Peter可以通知监听者的同时自己也能立即返回工作,让进程的线程池调用委托。然而不久他就发现监听者对其工作的评分丢掉了。这还了得!没有评定分数,我的绩效…Peter简直欲哭无泪.他立即采用缓存池(Polling)机制。Peter知道他做了一件明智的事并乐意Universe作为一个整体(不单单是他的boss)评判他。因此,Peter异步触发事件,但定期轮询,以察看可以获得的评分:
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
IAsyncResult res = wc.BeginInvoke(null, null);
while( !res.IsCompleted ) System.Threading.Thread.Sleep(1);
int grade = wc.EndInvoke(res);
Console.WriteLine("Worker grade= " + grade);
}
}
}
【译注:以下是上段程序输出结果
Worker: work started
Universe notices worker starting work
Worker: work progressing
Worker: work completed
Better...
Worker grade = 6
Universe pleased with worker's work
Worker grade = 7
Main: worker completed work //【译注:注意这个结果到最后才输出,下一节首句意思即是如此】
】
异步通知: 委托
----------------
不幸的是,Peter又倒退了—就象他一开始想避免boss站在一旁边监视他一样。也就是说,他现在要监看整个工作过程。因此,peter决定使用自己的委托作为异步委托完成时的通知方式,这样他就可以立即回去工作,而当工作被打分时,仍然可以接到通知:
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
wc.BeginInvoke(new AsyncCallback(WorkGraded), wc);
}
}
}
private void WorkGraded(IAsyncResult res) {
WorkCompleted wc = (WorkCompleted)res.AsyncState;
int grade = wc.EndInvoke(res);
Console.WriteLine("Worker grade= " + grade);
}
【译注:以下是上段程序输出结果
Worker: work started
Universe notices worker starting work
Worker: work progressing
Worker: work completed
Main: worker completed work //【译注:异步委托发生了效果,因此这一行先输出啦】
Better...
Worker grade = 6
Universe pleased with worker's work
Worker grade = 7
】
圆满结局
-----------
Peter、他的老板和Universe最终皆大欢喜,Peter的老板和Universe分别负责他们各自感兴趣的事件。这就减轻了执行负担和不必要的来回调用时间。Peter负责在发生事件时通知与事件关联的执行者,并最终异步获得结果,而不管他们做出回应时间的长短。但是,Peter也深知这一切并不像外表看起来那么简单,因为当他异步激活一个事件,与该事件关联的执行函数很可能是在另一个线程中执行,由此造成潜在的调度处理问题。不过Peter与Mike是好朋友,而Mike则精通线程问题的处理,他会为Peter不遗余力的。
现在一切OK!,关于Peter的故事就讲到这里,如果大家感兴趣的话,我还有很多故事要讲的!
……