C#/.NET中委托和事件的机制和应用

本文摘自人民邮电出版社出版的《Windows Forms程序设计》(Chris Sells著,荣耀、蒋贤哲译)。通过一个栩栩如生的虚构故事解释了C#/.NET中委托和事件的机制和应用。

1 委托

从前,在南方的一个异国他乡,有一个叫Peter的勤劳的工人,他对老板(boss)百依百顺,然而他的boss却是个卑鄙多疑的家伙,他坚 持要求Peter不断汇报工作进展。由于Peter不希望被boss盯着干活,于是他向boss承诺随时汇报工作进度。Peter通过如下所示的类型化的 引用(typed reference)定期回调boss来实现这个承诺:

c# 代码
  1. classWorker{
  2. publicvoidAdvise(Bossboss){this.boss=boss;}
  3. publicvoidDoWork(){
  4. Console.WriteLine("Worker:workstarted");
  5. if(boss!=null)boss.WorkStarted();
  6. Console.WriteLine("Worker:workprogressing");
  7. if(boss!=null)boss.WorkProgressing();
  8. Console.WriteLine("Worker:workcompleted");
  9. if(boss!=null){
  10. intgrade=boss.WorkCompleted();
  11. Console.WriteLine("Workergrade="+grade);
  12. }
  13. }
  14. Bossboss;
  15. }
  16. classBoss{
  17. publicvoidWorkStarted(){/*boss不关心*/}
  18. publicvoidWorkProgressing(){/*boss不关心*/}
  19. publicintWorkCompleted(){
  20. Console.WriteLine("It'sabouttime!");
  21. return2;/*10分以内*/
  22. }
  23. }
  24. classUniverse{
  25. staticvoidMain(){
  26. Workerpeter=newWorker();
  27. Bossboss=newBoss();
  28. peter.Advise(boss);
  29. peter.DoWork();
  30. Console.WriteLine("Main:workercompletedwork");
  31. Console.ReadLine();
  32. }
  33. }


1.1 接口

现在,Peter成了一个特殊人物,他不但能够忍受卑鄙的boss,和周围的世界(universe)也建立了紧密的联系。Peter感到 universe对他的工作进程同样感兴趣。不幸的是,如果不为universe添加一个特殊的Advise方法和特殊的回调,除了保证boss能够被通 知外,Peter并不能向universe通知工作进度。Peter希望能从那些通知方法的实现中分离出潜在的通知列表,为此,他决定将方法分离到一个接 口中:

c# 代码
  1. interfaceIWorkerEvents{
  2. voidWorkStarted();
  3. voidWorkProgressing();
  4. intWorkCompleted();
  5. }
  6. classWorker{
  7. publicvoidAdvise(IWorkerEventsevents){this.events=events;}
  8. publicvoidDoWork(){
  9. Console.WriteLine("Worker:workstarted");
  10. if(events!=null)events.WorkStarted();
  11. Console.WriteLine("Worker:workprogressing");
  12. if(events!=null)events.WorkProgressing();
  13. Console.WriteLine("Worker:workcompleted");
  14. if(events!=null){
  15. intgrade=events.WorkCompleted();
  16. Console.WriteLine("Workergrade="+grade);
  17. }
  18. }
  19. IWorkerEventsevents;
  20. }
  21. classBoss:IWorkerEvents{
  22. publicvoidWorkStarted(){/*boss不关心*/}
  23. publicvoidWorkProgressing(){/*boss不关心*/}
  24. publicintWorkCompleted(){
  25. Console.WriteLine("It'sabouttime!");
  26. return3;/*10分以内*/
  27. }
  28. }


1.2 委托

不幸的是,由于Peter忙于说服boss实现这个接口,以至于没有顾得上通知universe也实现该接口,但他希望尽可能做到这一点,至少他已经抽象了对boss的引用,因此,别的实现了IWorkerEvents接口的什么人也可以得到工作进度通知。

然而, Peter的boss仍然极其不满。“Peter!”boss咆哮者,“你为什么要通知我什么时候开始工作、什么时候正在进行工作?我不关心这些事件,你 不但强迫我实现这些方法,你还浪费了你的宝贵的工作时间等我从事件中返回。当我的实现需要占用很长时间时,你等我的时间也要大大延长!你难道不能想想别的 办法不要老是来烦我吗?”

因此,Peter意识到尽管在很多情况下接口很有用,但在处理事件时,接口的粒度还不够精细。他希望能做到仅仅通知监听者真正感兴趣的事件。为此,Peter决定把接口中的方法分解为若干个独立的委托函数,每一个都好象是只包含一个方法的微型接口:

c# 代码
  1. delegatevoidWorkStarted();
  2. delegatevoidWorkProgressing();
  3. delegateintWorkCompleted();
  4. classWorker{
  5. publicvoidDoWork(){
  6. Console.WriteLine("Worker:workstarted");
  7. if(started!=null)started();
  8. Console.WriteLine("Worker:workprogressing");
  9. if(progressing!=null)progressing();
  10. Console.WriteLine("Worker:workcompleted");
  11. if(completed!=null){
  12. intgrade=completed();
  13. Console.WriteLine("Workergrade="+grade);
  14. }
  15. }
  16. publicWorkStartedstarted;
  17. publicWorkProgressingprogressing;
  18. publicWorkCompletedcompleted;
  19. }
  20. classBoss{
  21. publicintWorkCompleted(){
  22. Console.WriteLine("Better...");
  23. return4;/*10分以内*/
  24. }
  25. }
  26. classUniverse{
  27. staticvoidMain(){
  28. Workerpeter=newWorker();
  29. Bossboss=newBoss();
  30. //注意:我们已将Advise方法替换为赋值运算符
  31. peter.completed=newWorkCompleted(boss.WorkCompleted);
  32. peter.DoWork();
  33. Console.WriteLine("Main:workercompletedwork");
  34. Console.ReadLine();
  35. }
  36. }


1.3 静态订阅者

利用委托,Peter达到了不拿boss不关心的事件去烦他的目标,然而Peter还是不能够使universe成为其订阅者之一。因为 universe是一个全封闭的实体,所以将委托挂钩在实例成员上不妥的(设想一下Universe的多个实例需要多少资源)。相反,Peter需要将委 托挂钩到静态成员上,因为委托也完全支持静态成员:

c# 代码
  1. classUniverse{
  2. staticvoidWorkerStartedWork(){
  3. Console.WriteLine("Universenoticesworkerstartingwork");
  4. }
  5. staticintWorkerCompletedWork(){
  6. Console.WriteLine("Universepleasedwithworker'swork");
  7. return7;
  8. }
  9. staticvoidMain(){
  10. Workerpeter=newWorker();
  11. Bossboss=newBoss();
  12. //注意:在下面的三行代码中,
  13. //使用赋值运算符不是一个好习惯,
  14. //请接着读下去,以便了解添加委托的正确方式。
  15. peter.completed=newWorkCompleted(boss.WorkCompleted);
  16. peter.started=newWorkStarted(Universe.WorkerStartedWork);
  17. peter.completed=newWorkCompleted(Universe.WorkerCompletedWork);
  18. peter.DoWork();
  19. Console.WriteLine("Main:workercompletedwork");
  20. Console.ReadLine();
  21. }
  22. }


2 事件

不幸的是,由于universe现在变得太忙并且不习惯于注意某一个人,universe已经设法用自己的委托取代了Peter的boss的 委托,这显然是将Worker类的委托字段设为public而造成的意外的副作用。同样,如果Peter的boss不耐烦了,他自己就可以触发Peter 的委托(Peter的boss可是有暴力倾向的)

// Peter的boss自己控制一切
if( peter.completed != null ) peter.completed();

Peter希望确保不会发生这两种情况。他意识到必须为每一个委托加入注册和反注册函数,这样订阅者就可以添加或移去它们自个儿,但谁都不能 够清空整个事件列表或者触发它的事件。peter自己没去实现这些方法,相反,他使用event关键字让C#编译器帮他构建这些方法:

class Worker {
...
public event WorkStarted started;
public event WorkProgressing progressing;
public event WorkCompleted completed;
}

Peter晓得event关键字使委托具有这样的属性:只允许C#客户用+=或-=操作符添加或移去它们自己,这样就迫使boss和universe举止文雅一些:

c# 代码
  1. staticvoidMain(){
  2. Workerpeter=newWorker();
  3. Bossboss=newBoss();
  4. peter.completed+=newWorkCompleted(boss.WorkCompleted);
  5. peter.started+=newWorkStarted(Universe.WorkerStartedWork);
  6. peter.completed+=newWorkCompleted(Universe.WorkerCompletedWork);
  7. peter.DoWork();
  8. Console.WriteLine("Main:workercompletedwork");
  9. Console.ReadLine();
  10. }


2.1 获取所有结果

至此,Peter终于松了一口气。他已经设法满足了所有订阅者的需求,而且不会和特定实现紧密耦合。然而,他又注意到尽管boss和 universe都为他的工作打了分,但他只得到了一个打分。在有多个订阅者的情形下,Peter希望能得到所有订阅者的评分结果。因此,他决定“进入委 托”,提取订阅者列表,以便手工分别调用它们:

public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
int grade = wc();
Console.WriteLine("Worker grade= " + grade);
}
}
}

2.2 异步通知:触发和忽略

不料,在此期间,boss和universe被别的什么事纠缠上了,这就意味着他们给Peter的工作打分的时间被大大延长了:

class Boss {
public int WorkCompleted() {
System.Threading.Thread.Sleep(3000);
Console.WriteLine("Better..."); return 6; /* 10分以内 */
}
}

class Universe {
static int WorkerCompletedWork() {
System.Threading.Thread.Sleep(4000);
Console.WriteLine("Universe is pleased with worker's work");
return 7;
}
...
}

不幸的是,由于Peter是同时通知每一个订阅者并等待他们打分的,这些需要返回评分的通知现在看来要占用他不少工作时间,因此,Peter决定忽略评分并且异步触发事件:

public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
wc.BeginInvoke(null, null);
}
}
}

2.3 异步通知:轮询

这个聪明的小把戏允许Peter在通知订阅者的同时能立即返回工作,让进程的线程池调用委托。然而没过多久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);
}
}
}

2.4 异步通知:委托

不幸的是,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);
}
}
}

void WorkGraded(IAsyncResult res) {
WorkCompleted wc = (WorkCompleted)res.AsyncState;
int grade = wc.EndInvoke(res);
Console.WriteLine("Worker grade= " + grade);
}

3 普天同乐

Peter、boss和universe最终都满意了。boss和universe都可以仅被通知其感兴趣的事件,并减少了实现的负担和不必 要的来回调用。Peter可以通知他们每一个人,而不必管需要多长时间才能从那些目标方法中返回,并仍然可以异步得到评分结果。结果得到如下完整的解决方 案:

c# 代码
  1. delegatevoidWorkStarted();
  2. delegatevoidWorkProgressing();
  3. delegateintWorkCompleted();
  4. classWorker{
  5. publicvoidDoWork(){
  6. Console.WriteLine("Worker:workstarted");
  7. if(started!=null)started();
  8. Console.WriteLine("Worker:workprogressing");
  9. if(progressing!=null)progressing();
  10. Console.WriteLine("Worker:workcompleted");
  11. if(completed!=null){
  12. foreach(WorkCompletedwcincompleted.GetInvocationList()){
  13. wc.BeginInvoke(newAsyncCallback(WorkGraded),wc);
  14. }
  15. }
  16. }
  17. voidWorkGraded(IAsyncResultres){
  18. WorkCompletedwc=(WorkCompleted)res.AsyncState;
  19. intgrade=wc.EndInvoke(res);
  20. Console.WriteLine("Workergrade="+grade);
  21. }
  22. publiceventWorkStartedstarted;
  23. publiceventWorkProgressingprogressing;
  24. publiceventWorkCompletedcompleted;
  25. }
  26. classBoss{
  27. publicintWorkCompleted(){
  28. System.Threading.Thread.Sleep(3000);
  29. Console.WriteLine("Better...");return6;/*10分以内*/
  30. }
  31. }
  32. classUniverse{
  33. staticvoidWorkerStartedWork(){
  34. Console.WriteLine("Universenoticesworkerstartingwork");
  35. }
  36. staticintWorkerCompletedWork(){
  37. System.Threading.Thread.Sleep(4000);
  38. Console.WriteLine("Universeispleasedwithworker'swork");
  39. return7;
  40. }
  41. staticvoidMain(){
  42. Workerpeter=newWorker();
  43. Bossboss=newBoss();
  44. peter.completed+=newWorkCompleted(boss.WorkCompleted);
  45. peter.started+=newWorkStarted(Universe.WorkerStartedWork);
  46. peter.completed+=newWorkCompleted(Universe.WorkerCompletedWork);
  47. peter.DoWork();
  48. Console.WriteLine("Main:workercompletedwork");
  49. Console.ReadLine();
  50. }
  51. }


Peter知道异步获取结果会带来一些问题。由于异步触发事件,所以目标方法有可能执行于另一个线程中,就像Peter的“目标方法何时完 成”的通知那样。然而,Peter熟悉第14章“多线程用户界面”,因此,他知道在构建WinForms应用程序时如何去处理此类问题。

从此,他们一直过得都很快乐。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值