委託和事件— 一個虛構的故事

委託和事件— 一個虛構的故事

--------------------------------------------------------------------------------
 
http://www.pcdog.com 2004-12-11 互聯網
 
    
  本文摘自人民郵電出版社出版的《Windows Forms程序設計》(Chris Sells著,榮耀、蔣賢哲譯)。通過一個栩栩如生的虛構故事解釋了C#/.NET中委託和事件的機制和應用。
  
  1 委託

  
  從前,在南方的一個異國他鄉,有一個叫Peter的勤勞的工人,他對老闆(boss)百依百順,然而他的boss卻是個卑鄙多疑的傢伙,他堅持要求Peter不斷彙報工作進展。由於Peter不希望被boss盯着幹活,於是他向boss承諾隨時彙報工作進度。Peter通過如下所示的類型化的引用(typed reference)定期回調boss來實現這個承諾: 
   


  
class Worker 
   
public void Advise(Boss boss) {this.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); 
   }
 
   }
 
   Boss boss; 
  }
 
   
  
class Boss 
   
public void WorkStarted() {/* boss不關心 */ } 
   
public void WorkProgressing() {/* boss不關心 */ } 
   
public int WorkCompleted() 
   Console.WriteLine(
"It's about time!"); 
   
return 2/* 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(); 
   }
 
  }
 

   
  1.1 接口
  
  現在,Peter成了一個特殊人物,他不但能夠忍受卑鄙的boss,和周圍的世界(universe)也建立了緊密的聯繫。Peter感到universe對他的工作進程同樣感興趣。不幸的是,如果不爲universe添加一個特殊的Advise方法和特殊的回調,除了保證boss能夠被通知外,Peter並不能向universe通知工作進度。Peter希望能從那些通知方法的實現中分離出潛在的通知列表,爲此,他決定將方法分離到一個接口中: 
   


  
interface IWorkerEvents 
   
void WorkStarted(); 
   
void WorkProgressing(); 
   
int WorkCompleted(); 
  }
 
   
  
class Worker 
   
public void Advise(IWorkerEvents events) {this.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); 
   }
 
   }
 
   IWorkerEvents events; 
  }
 
   
  
class Boss : IWorkerEvents 
   
public void WorkStarted() {/* boss不關心 */ } 
   
public void WorkProgressing() {/* boss不關心 */ } 
   
public int WorkCompleted() 
   Console.WriteLine(
"It's about time!"); 
   
return 3/* 10分以內 */ 
   }
 
  }
 
   


  1.2 委託
  
  不幸的是,由於Peter忙於說服boss實現這個接口,以至於沒有顧得上通知universe也實現該接口,但他希望儘可能做到這一點,至少他已經抽象了對boss的引用,因此,別的實現了IWorkerEvents接口的什麼人也可以得到工作進度通知。
  
  然而, Peter的boss仍然極其不滿。“Peter!”boss咆哮者,“你爲什麼要通知我什麼時候開始工作、什麼時候正在進行工作?我不關心這些事件,你不但強迫我實現這些方法,你還浪費了你的寶貴的工作時間等我從事件中返回。當我的實現需要佔用很長時間時,你等我的時間也要大大延長!你難道不能想想別的辦法不要老是來煩我嗎?”
  
  因此,Peter意識到儘管在很多情況下接口很有用,但在處理事件時,接口的粒度還不夠精細。他希望能做到僅僅通知監聽者真正感興趣的事件。爲此,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/* 10分以內 */ 
   }
 
  }
 
   
  
class Universe 
   
static void Main() 
   Worker peter 
= new Worker(); 
   Boss boss 
= new Boss(); 
   
   
// 注意:我們已將Advise方法替換爲賦值運算符 
   peter.completed = new WorkCompleted(boss.WorkCompleted); 
   peter.DoWork(); 
   Console.WriteLine(
"Main: worker completed work"); 
   Console.ReadLine(); 
   }
 
  }
 
   


  1.3 靜態訂閱者
  
  利用委託,Peter達到了不拿boss不關心的事件去煩他的目標,然而Peter還是不能夠使universe成爲其訂閱者之一。因爲universe是一個全封閉的實體,所以將委託掛鉤在實例成員上不妥的(設想一下Universe的多個實例需要多少資源)。相反,Peter需要將委託掛鉤到靜態成員上,因爲委託也完全支持靜態成員: 
   


  
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); 
   peter.DoWork(); 
   
   Console.WriteLine(
"Main: worker completed work"); 
   Console.ReadLine(); 
   }
 
  }
 
   


  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舉止文雅一些: 
   


  
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(); 
  }
 
   


  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(
nullnull); 
   }
 
   }
 
  }
 
   


  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(nullnull); 
   
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可以通知他們每一個人,而不必管需要多長時間才能從那些目標方法中返回,並仍然可以異步得到評分結果。結果得到如下完整的解決方案: 
   


  
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 ) 
   
   
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); 
   }
 
   
   
public event WorkStarted started; 
   
public event WorkProgressing progressing; 
   
public event WorkCompleted completed; 
  }
 
   
  
class Boss 
   
public int WorkCompleted() 
   System.Threading.Thread.Sleep(
3000); 
   Console.WriteLine(
"Better..."); return 6/* 10分以內 */ 
   }
 
  }
 
   
  
class Universe 
   
static void WorkerStartedWork() 
   Console.WriteLine(
"Universe notices worker starting work"); 
   }
 
   
   
static int WorkerCompletedWork() 
   System.Threading.Thread.Sleep(
4000); 
   Console.WriteLine(
"Universe is 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); 
   peter.DoWork(); 
   
   Console.WriteLine(
"Main: worker completed work"); 
   Console.ReadLine(); 
   }
 
  }
 
   


  Peter知道異步獲取結果會帶來一些問題。由於異步觸發事件,所以目標方法有可能執行於另一個線程中,就像Peter的“目標方法何時完成”的通知那樣。然而,Peter熟悉第14章“多線程用戶界面”,因此,他知道在構建WinForms應用程序時如何去處理此類問題。
  
  從此,他們一直過得都很快樂。  
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章