委託和事件— 一個虛構的故事
--------------------------------------------------------------------------------
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(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可以通知他們每一個人,而不必管需要多長時間才能從那些目標方法中返回,並仍然可以異步得到評分結果。結果得到如下完整的解決方案:
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應用程序時如何去處理此類問題。
從此,他們一直過得都很快樂。