目錄
- 事件、事件處理常式概念
- 問題描述:一個需要較長時間才能完成的任務
- 高耦合的實現
- 事件模型的解決方案,簡單易懂的 VB.NET 版本
- 委託(delegate)簡介
- C# 實現
- 向“.NET Framework 類庫設計指南”靠攏,標準實現
事件、事件處理常式概念
在物件導向理論中,一個對象(類的執行個體)可以有屬性(property,擷取或設定對象的狀態)、方法(method,對象可以做的動作)等成員外,還有事件(event)。所謂事件,是對象內部狀態發生了某些變化、或者對象做某些動作時(或做之前、做之後),向外界發出的通知。打個比方就是,對象“張三”肚子疼了,然後他站在空地上大叫一聲“我肚子疼了!”事件就是這個通知。
那麼,相對於對象內部發出的事件通知,外部環境可能需要應對某些事件的發生,而做出相應的反應。接著上面的比方,張三大叫一聲之後,救護車來了把它 接到醫院(或者瘋人院,呵呵,開個玩笑)。外界因應事件發生而做出的反應(具體到程式上,就是針對該事件而寫的那些處理代碼),稱為事件處理常式(event handler)。
事件處理常式必須和對象的事件掛鈎後,才可能會被執行。否則,孤立的事件處理常式不會被執行。另一方面,對象發生事件時,並不一定要有相應的處理程 序。就如張三大叫之後,外界環境沒有做出任何反應。也就是說,對象的事件和外界對該對象的事件處理之間,並沒有必然的聯絡,需要你去掛接。
在開始學習之前,我希望大家首先區分“事件”和“事件處理常式”這兩個概念。事件是隸屬於對象(類)本身的,事件處理常式是外界代碼針對對象的事件 做出的反應。事件,是對象(類)的設計者、開發人員應該完成的;事件處理常式是外界調用方需要完成的。簡單的說,事件是“內”;事件處理常式是“外”。
瞭解以上基本概念之後,我們開始學習具體的代碼實現過程。因為涉及代碼比較多,限於篇幅,我只是將代碼中比較重要的部分貼在文章裡,進行解析,剩餘代碼還是請讀者自己查閱,我已經把原始碼打了包提供下載。我也建議你對照這些原始碼,來學習教程。[下載本教程的原始碼]
[TOP]
問題描述:一個需要較長時間才能完成的任務
Demo 1A,問題描述。這是一個情景示範,也是本教程中其他 Demo 都致力於解決的一個“實際問題”:Worker 類中有一個可能需要較長時間才能完成的方法 DoLongTimeTask:
using System;using System.Threading;namespace percyboy.EventModelDemo.Demo1A{ // 需要做很長時間才能完成任務的 Worker,沒有加入任何彙報途徑。 public class Worker { // 請根據你的機器配置情況,設定 MAX 的值。 // 在我這裡(CPU: AMD Sempron 2400+, DDRAM 512MB) // 當 MAX = 10000,任務耗時 20 秒。 private const int MAX = 10000; public Worker() { } public void DoLongTimeTask() { int i; bool t = false; for (i = 0; i <= MAX; i++) { // 此處 Thread.Sleep 的目的有兩個: // 一個是不讓 CPU 時間全部耗費在這個任務上: // 因為本例中的工作是一個純粹消耗 CPU 計算資源的任務。 // 如果一直讓它一直佔用 CPU,則 CPU 時間幾乎全部都耗費於此。 // 如果任務時間較短,可能影響不大; // 但如果任務耗時也長,就可能會影響系統中其他任務的正常運行。 // 所以,Sleep 就是要讓 CPU 有機會“分一下心”, // 處理一下來自其他任務的計算請求。 // // 當然,這裡的主要目的是為了讓這個任務看起來耗時更長一點。 Thread.Sleep(1); t = !t; } } }}
介面很簡單(本教程中其他 Demo 也都沿用這個介面,因為我們主要的研究對象是 Worker.cs):
單擊“Start”按鈕後,開始執行該方法。(具體的機器配置條件,完成此任務需要的時間也不同,你可以根據你的實際情況調整代碼中的 MAX 值。)
在沒有進度指示的情況下,介面長時間的無響應,往往會被使用者認為是程式故障或者“死機”,而實際上,你的工作進行中還沒有結束。此次教程就是以解決此問題為執行個體,向你介紹 .NET 中事件模型的原理、設計與具體編碼實現。
[TOP]
高耦合的實現
Demo 1B,高度耦合。有很多辦法可以讓 Worker 在工作的時候向使用者介面報告進度,比如最容易想到的:
public void DoLongTimeTask() { int i; bool t = false; for (i = 0; i <= MAX; i++) { Thread.Sleep(1); t = !t; 在此處書寫重新整理使用者介面狀態列的代碼 } }
如果說 DoLongTimeTask 是使用者介面(Windows 表單)的一個方法,那麼上面藍色部分或許很簡單,可能只不過是如下的兩行代碼:
double rate = (double)i / (double)MAX; this.statusbar.Text = String.Format(@"已完成 {0:P2} ...", rate);
不過這樣的話,DoLongTimeTask 就是這個 Windows 表單的一部分了,顯然它不利於其他表單調用這段代碼。那麼:Worker 類應該作為一個相對獨立的部分存在。原始碼 Demo1B 中給出了這樣的一個樣本(應該還有很多種、和它類似的方法):
Windows 表單 Form1 中單擊“Start”按鈕後,初始化 Worker 類的一個新執行個體,並執行它的 DoLongTimeTask 方法。但你應該同時看到,Form1 也賦值給 Worker 的一個屬性,在 Worker 執行 DoLongTimeTask 方法時,通過這個屬性重新整理 Form1 的狀態列。Form1 和 Worker 之間相互粘在一起:Form1 依賴於 Worker 類(因為它單擊按鈕後要執行個體化 Worker),Worker 類也依賴於 Form1(因為它在工作時,需要訪問 Form1)。這二者之間形成了高度耦合。
高度耦合約樣不利於代碼重用,你仍然無法在另一個表單裡使用 Worker 類,代碼靈活度大為降低。正確的設計原則應該是努力實現低耦合:如果 Form1 必須依賴於 Worker 類,那麼 Worker 類就不應該再反過來依賴於 Form1。
下面我們考慮使用 .NET 事件模型解決上述的“高度耦合”問題:
讓 Worker 類在工作時,向外界發出“進度報告”的事件通知(RateReport)。同時,為了示範更多的情景,我們讓 Worker 類在開始 DoLongTimeTask 之前發出一個“我要開始幹活了!總任務數有 N 件。”的事件通知(StartWork),並在完成任務時發出“任務完成”的事件通知(EndWork)。
採用事件模型後,類 Worker 本身並不實際去重新整理 Form1 的狀態列,也就是說 Worker 不依賴於 Form1。在 Form1 中,單擊“Start”按鈕後,Worker 的一個執行個體開始工作,並發出一系列的事件通知。我們需要做的是為 Worker 的事件書寫事件處理常式,並將它們掛接起來。
[TOP]
事件模型的解決方案,簡單易懂的 VB.NET 版本
Demo 1C,VB.NET 代碼。雖然本教程以 C# 為樣本語言,我還是給出一段 VB.NET 的代碼輔助大家的理解。因為我個人認為 VB.NET 的事件文法,能讓你非常直觀的領悟到 .NET 事件模型的“思維方式”:
Public Class Worker Private Const MAX = 10000 Public Sub New() End Sub ' 註:此例的寫法不符合 .NET Framework 類庫設計指南中的約定, ' 只是為了讓你快速理解事件模型而簡化的。 ' 請繼續閱讀,使用 Demo 1F 的 VB.NET 標準寫法。 ' ' 工作開始事件,並同時通知外界需要完成的數量。 Public Event StartWork(ByVal totalUnits As Integer) ' 進度彙報事件,通知外界任務完成的進度情況。 Public Event RateReport(ByVal rate As Double) ' 工作結束事件。 Public Event EndWork() Public Sub DoLongTimeTask() Dim i As Integer Dim t As Boolean = False Dim rate As Double ' 開始工作前,向外界發出事件通知 RaiseEvent StartWork(MAX) For i = 0 To MAX Thread.Sleep(1) t = Not t rate = i / MAX RaiseEvent RateReport(rate) Next RaiseEvent EndWork() End Sub
首先是事件的聲明部分:你只需寫上 Public Event 關鍵字,然後寫事件的名稱,後面的參數部分寫上需要發送到外界的參數聲明。
然後請注意已標記為藍色的 RaiseEvent 關鍵字,VB.NET 使用此關鍵字在類內部引發事件,也就是向外界發送事件通知。請注意它的文法,RaiseEvent 後接上你要引發的事件名稱,然後是具體的事件參數值。
從這個例子中,我們可以加深對事件模型的認識:事件是對象(類)的成員,在對象(類)內部狀態發生了一些變化(比如此例中 rate 在變化),或者對象做一些動作時(比如此例中,方法開始時,向外界 raise event;方法結束時,向外界 raise event),對象(類)發出的通知。並且,你也瞭解了事件參數的用法:事件參數是事件通知的相關內容,比如 RateReport 事件通知需要報告進度值 rate,StartWork 事件通知需要報告總任務數 MAX。
我想 RaiseEvent 很形象的說明了這些道理。
[TOP]
委託(delegate)簡介。
在學習 C# 實現之前,我們首先應該瞭解一些關於“委託”的基礎概念。
你可以簡單的把“委託(delegate)”理解為 .NET 對函數的封裝(這是委託的主要用途)。委託代表一“類”函數,它們都符合一定的規格,如:擁有相同的參數個數、參數類型、傳回值類型等。也可以認為委託是 對函數的抽象,是函數的“類”(類是具有某些相同特徵的事物的抽象)。這時,委託的執行個體將代表一個具體的函數。
你可以用如下的方式聲明委託:
public delegate void MyDelegate(int integerParameter);
如上的委託將可以用於代表:有且只有一個整數型參數、且不帶傳回值的一組函數。它的寫法和一個函數的寫法類似,只是多了 delegate 關鍵字、而沒有函數體。(註:本文中的函數(function),取了面向過程理論中慣用的術語。在完全物件導向的 .NET/C# 中,我用以指代類的執行個體方法或靜態方法(method),希望不會因此引起誤解。順帶地,既然完全物件導向,其實委託本身也是一種對象。)
委託的執行個體化:既然委託是函數的“類”,那麼使用委託之前也需要執行個體化。我們先看如下的代碼:
public class Sample { public void DoSomething(int mode) { Console.WriteLine("test function."); } public static void Hello(int world) { Console.WriteLine("hello, world!"); } }
我們看到 Sample 的執行個體方法 DoSomething 和靜態方法 Hello 都符合上面已經定義了的 MyDelegate 委託的“規格”。那麼我們可以使用 MyDelegate 委託來封裝它們,以用於特殊的用途(比如下面要講的事件模型,或者將來教程中要講的多執行緒模式)。當然,封裝的過程其實也是委託的執行個體化過程:
Sample sp = new Sample(); MyDelegate del = new MyDelegate(sp.DoSomething);
這是對上面的執行個體方法的封裝。但如果這段代碼寫在 Sample 類內部,則應使用 this.DoSomething 而不用建立一個 Sample 執行個體。對 Sample 的 Hello 靜態方法可以封裝如下:
MyDelegate del = new MyDelegate(Sample.Hello);
調用委託:對於某個委託的執行個體(其實是一個具體的函數),如果想執行它:
del(12345);
直接寫上委託執行個體的名字,並在括弧中給相應的參數賦值即可。(如果函數有傳回值,也可以像普通函數那樣接收傳回值)。
[TOP]
C# 實現
Demo 1D,C# 實現。這裡給出 Demo 1C 中 VB.NET 代碼的 C# 實現:是不是比 VB.NET 的代碼複雜了一些呢?
using System;using System.Threading;namespace percyboy.EventModelDemo.Demo1D{ // 需要做很長時間才能完成任務的 Worker,這次我們使用事件向外界通知進度。 public class Worker { private const int MAX = 10000; // 註:此例的寫法不符合 .NET Framework 類庫設計指南中的約定, // 只是為了讓你快速理解事件模型而簡化的。 // 請繼續閱讀,使用 Demo 1E / Demo 1H 的 C# 標準寫法。 // public delegate void StartWorkEventHandler(int totalUnits); public delegate void EndWorkEventHandler(); public delegate void RateReportEventHandler(double rate); public event StartWorkEventHandler StartWork; public event EndWorkEventHandler EndWork; public event RateReportEventHandler RateReport; public Worker() { } public void DoLongTimeTask() { int i; bool t = false; double rate; if (StartWork != null) { StartWork(MAX); } for (i = 0; i <= MAX; i++) { Thread.Sleep(1); t = !t; rate = (double)i / (double)MAX; if (RateReport != null) { RateReport(rate); } } if (EndWork != null) { EndWork(); } } }}
這份代碼和上面 VB.NET 代碼實現一致的功能。通過 C# 代碼,我們可以看到被 VB.NET 隱藏了的一些實現細節:
首先,這裡一開始聲明了幾個委託(delegate)。然後聲明了三個事件,這裡請注意 C# 事件聲明的方法:
public event [委託類型] [事件名稱];
這裡你可以看到 VB.NET 隱藏了聲明委託的步驟。
另外提醒你注意代碼中具體引發事件的部分:
if (RateReport != null) { RateReport(rate); }
在調用委託之前,必須檢查委託是否為 null,否則將有可能引發 NullReferenceException 意外;比較 VB.NET 的代碼,VB.NET 的 RaiseEvent 語句實際上也隱藏了這一細節。
好了,到此為止,Worker 類部分通過事件模型向外界發送事件通知的功能已經有了第一個版本,修改你的 Windows 表單,給它添加 RateReport 事件處理常式(請參看你已下載的原始碼),並掛接到一起,看看現在的效果:
添加了進度指示之後的介面,極大的改善了使用者體驗,對使用者更為友好。
[TOP]
向“.NET Framework 類庫設計指南”靠攏,標準實現
Demo 1E,C# 的標準實現。上文已經反覆強調了 Demo 1C, Demo 1D 代碼不符合 CLS 規範 約定。微軟為 .NET 類庫的設計與命名提出了一些指南,作為一種約定,.NET 開發人員應當遵守這些約定。涉及事件的部分,請參看事件命名指南(對應的線上網頁),事件使用指南(對應的線上網頁)。
using System;using System.Threading;namespace percyboy.EventModelDemo.Demo1E{ public class Worker { private const int MAX = 10000; public class StartWorkEventArgs : EventArgs { private int totalUnits; public int TotalUnits { get { return totalUnits; } } public StartWorkEventArgs(int totalUnits) { this.totalUnits = totalUnits; } } public class RateReportEventArgs : EventArgs { private double rate; public double Rate { get { return rate; } } public RateReportEventArgs(double rate) { this.rate = rate; } } public delegate void StartWorkEventHandler(object sender, StartWorkEventArgs e); public delegate void RateReportEventHandler(object sender, RateReportEventArgs e); public event StartWorkEventHandler StartWork; public event EventHandler EndWork; public event RateReportEventHandler RateReport; protected virtual void OnStartWork( StartWorkEventArgs e ) { if (StartWork != null) { StartWork(this, e); } } protected virtual void OnEndWork( EventArgs e ) { if (EndWork != null) { EndWork(this, e); } } protected virtual void OnRateReport( RateReportEventArgs e ) { if (RateReport != null) { RateReport(this, e); } } public Worker() { } public void DoLongTimeTask() { int i; bool t = false; double rate; OnStartWork(new StartWorkEventArgs(MAX) ); for (i = 0; i <= MAX; i++) { Thread.Sleep(1); t = !t; rate = (double)i / (double)MAX; OnRateReport( new RateReportEventArgs(rate) ); } OnEndWork( EventArgs.Empty ); } }}
按照 .NET Framework 類庫設計指南中的約定:
(1)事件委託名稱應以 EventHandler 為結尾;
(2)事件委託的“規格”應該是兩個參數:第一個參數是 object 類型的 sender,代表發出事件通知的對象(代碼中一般是 this 關鍵字(VB.NET 中是 Me))。第二個參數 e,應該是 EventArgs 類型或者從 EventArgs 繼承而來的類型;
事件參數類型,應從 EventArgs 繼承,名稱應以 EventArgs 結尾。應該將所有想通過事件、傳達到外界的資訊,放在事件參數 e 中。
(3)一般的,只要類不是密封(C# 中的 sealed,VB.NET 中的 NotInheritable)的,或者說此類可被繼承,應該為每個事件提供一個 protected 並且是可重寫(C# 用 virtual,VB.NET 用 Overridable)的 OnXxxx 方法:該方法名稱,應該是 On 加上事件的名稱;只有一個事件參數 e;一般在該方法中進行 null 判斷,並且把 this/Me 作為 sender 執行事件委託;在需要發出事件通知的地方,應調用此 OnXxxx 方法。
對於此類的子類,如果要改變發生此事件時的行為,應重寫 OnXxxx 方法;並且在重寫時,一般情況下應調用基類的此方法(C# 裡的 base.OnXxxx,VB.NET 用 MyBase.OnXxxx)。
我建議你能繼續花些時間研究一下這份代碼的寫法,它是 C# 的標準事件實現代碼,相信你會用得著它!
在 Demo 1D 中我沒有講解如何將事件處理常式掛接到 Worker 執行個體的事件的代碼,在這個 Demo 中,我將主要的部分列在這裡:
private void button1_Click(object sender, System.EventArgs e) { statusBar1.Text = "開始工作 ...."; this.Cursor = Cursors.WaitCursor; long tick = DateTime.Now.Ticks; Worker worker = new Worker(); // 將事件處理常式與 Worker 的相應事件掛鈎 // 這裡我只掛鈎了 RateReport 事件做示意 worker.RateReport += new Worker.RateReportEventHandler(this.worker_RateReport); worker.DoLongTimeTask(); tick = DateTime.Now.Ticks - tick; TimeSpan ts = new TimeSpan(tick); this.Cursor = Cursors.Default; statusBar1.Text = String.Format("任務完成,耗時 {0} 秒。", ts.TotalSeconds); } private void worker_RateReport(object sender, Worker.RateReportEventArgs e) { this.statusBar1.Text = String.Format("已完成 {0:P0} ....", e.Rate); }
請注意 C# 的掛接方式(“+=”運算子)。
到這裡為此,你已經看到了事件機制的好處:Worker 類的代碼和這個 Windows Form 沒有依賴關係。Worker 類可以單獨存在,可以被重複應用到不同的地方。
VB.NET 的讀者,請查看 Demo 1F 中的 VB.NET 標準事件寫法,並參考這裡的說明,我就不再贅述了。
引用自 : http://www.iwms.net/n1357c13.aspx