深入探討事件 (Event)
使用 C# |
Eric Gunnerson
Microsoft Corporation
2001 年 5 月 17 日
下載本專欄的範例程式碼
注意 這個範例程式碼在上個月的專欄中用過。如果您上個月有下載,就不必再下載了。
上個月我們談到委派如何運作,也提了一下事件。這個月,我們要深入一點探討事件。
在上個月的專欄中,我提示過事件就像只有套用 +=
和 -=
運算子的委派。我們學到當您以下列方式使用委派時:
MyClass.LogHandler lh = null; lh += new MyClass.LogHandler(Logger); lh += new MyClass.LogHandler(fl.Logger);
編譯器會將 +=
的使用轉換為呼叫 Delegate.Combine()。因此,我們或許可以預期,同樣的情形也會發生在事件上,且讓我們來看看編譯器會做什麼。我們用的是上個月的程式碼:
public static void Main() { MyObject myObject = new MyObject(); myObject.Click += new MyObject.ClickHandler(ClickFunction); }
建立程式碼之後,經由 Microsoft 中繼語言反組譯工具 (Intermediate Language Disassembler,ILDASM) 加以執行 (如需 ILDASM 的詳細資訊,請參閱易學易用 C#),看看那兒有些什麼東西。令我們感到驚訝的是,找到的不是對 Delegate.Combine() 的呼叫,而是對 MyObject 類別中名為 add_Click() 的函式的呼叫。嗯,我真想知道那個函式會做什麼。
如果查看一下 MyObject 的資訊,會注意到,它有名為 add_Click() 和 remove_Click() 的函式。查看 add_Click() 中的 IL 時,發現函式中有對 Delegate.Combine() 的呼叫。
這種情形和屬性的情形很像,我們不是直接存取欄位,而是用存取子 (Accessor) 函式取得或設定數值。在這個案例中,我們呼叫一個加入函式以連結事件,再呼叫移除函式從事件取消攔截,這正是為什麼在事件上除了 +=
或 -=
之外,您不能作其他事情的原因。我們所謂的事件會被實作為加入和移除函式。
C# 中有兩種不同的方法可以實作事件,這兩種方法的詳細情形略有不同。首先來談談簡單的案例。
簡單的事件
在簡單事件案例中,編譯器會為我們做所有的工作。當我們撰寫底下這樣的程式碼時:
public event LogHandler Log;
編譯器會宣告名為 Log 的私用 (Private) 委派欄位,也會宣告名為 add_Log() 和 remove_Log() 的函式,這些函式會呼叫適當的 Delegate 函式,以便從私用欄位加入或移除委派。最後則是在參考加入和移除函式的中繼資料中宣告名為 Log 的事件,這樣就會有事件顯示在物件瀏覽器中,而且編譯器還可以找到和事件相關聯的加入和移除函式 (這也是屬性運作的方式)。
且讓我們修改上個月的記錄範例,以便使用事件而不是使用委派:
using System; using System.IO; public class MyClass { public delegate void LogHandler(string message); public event LogHandler Log; public void Process() { OnLog("Process() begin"); // other stuff here? OnLog("Process() end"); } protected void OnLog(string message) { if (Log != null) Log(message); } } class FileLogger { FileStream fileStream; StreamWriter streamWriter; public FileLogger(string filename) { fileStream = new FileStream(filename, FileMode.Create); streamWriter = new StreamWriter(fileStream); } public void Logger(string s) { streamWriter.WriteLine(s); } public void Close() { streamWriter.Close(); fileStream.Close(); } } class Test { static void Logger(string s) { Console.WriteLine(s); } public static void Main() { FileLogger fl = new FileLogger("process.log"); MyClass myClass = new MyClass(); myClass.Log += new MyClass.LogHandler(Logger); myClass.Log += new MyClass.LogHandler(fl.Logger); myClass.Process(); fl.Close(); } }
我們不讓 Process() 函式以委派作為參數,而是宣告 Log 事件。在 Main() 中,只是將委派的執行個體加入事件會比較簡潔一些,您不必自己去處理事情 (或許還會做錯了)。
通常您都會想讓編譯器去做所有的事情,不過,有時候還是需要自己動手。
進階事件
編譯器處理事件時的問題之一是,它會為事件宣告委派欄位,而這會在物件中佔用空間。如果物件只支援一個或兩個事件,還不成問題,但是很多使用者介面物件可能支援十個、甚至上百個不同的事件。指定的物件同時只能連結幾個事件,所以會有很多浪費的空間。
進階語法可以讓程式設計人員接管整個處理序,包括委派的儲存。若要開始試試,底下的進階語法能執行與編譯器完全一樣的功能:
using System; using System.Runtime.CompilerServices; public class MyClass { public delegate void LogHandler(string message); private LogHandler log; public event LogHandler Log { [MethodImpl(MethodImplOptions.Synchronized)] add { log = (LogHandler) Delegate.Combine(log, value); } [MethodImpl(MethodImplOptions.Synchronized)] remove { log = (LogHandler) Delegate.Remove(log, value); } } protected void OnLog(string message) { if (log != null) log(message); } }
在這個範例中,我們為事件撰寫了自己的 add() 和 remove() 存取子。Delegate.Combine() 和 Delegate.Remove() 呼叫不需要太多解釋。必須要有 MethodImpl 屬性,加入和移除函式才是安全執行緒的 (Thread-safe) 函式。如果沒有這個屬性,兩個執行緒可能會同時呼叫加入函式,如果時機湊巧 (或者不湊巧,看您的觀點而定),可能會得到不正確的結果。
這個範例顯示的是,如何撰寫沒有任何好處的多餘程式碼。為了得到我們所要的好處 - 使用較少的儲存空間 - 我們需要用其他方法儲存委派。
假設我們的物件可以有很多事件和它關聯,但是這些事件很少是在作用中。所以,我們將需要方法來儲存和事件關聯的委派,而不必讓各個委派都要使用獨立的欄位。將它們儲存在雜湊表 (Hashtable) 中是輕鬆的作法,但是撰寫類別將雜湊表封裝起來更簡潔一些,介面也會比較簡單。最後的結果如下:
using System; using System.Runtime.CompilerServices; using System.Collections; class DelegateCache { private Hashtable delegateStorage = new Hashtable(); public Delegate Find(object key) { return((Delegate) delegateStorage[key]); } public void Add(object key, Delegate myDelegate) { delegateStorage[key] = Delegate.Combine((Delegate) delegateStorage[key], myDelegate); } public void Remove(object key, Delegate myDelegate) { delegateStorage[key] = Delegate.Remove((Delegate) delegateStorage[key], myDelegate); } } public class MyClass { public delegate void LogHandler(string message); private DelegateCache delegateCache = new DelegateCache(); private static object logEventKey = new object(); // unique key public event LogHandler Log { [MethodImpl(MethodImplOptions.Synchronized)] add { delegateCache.Add(logEventKey, value); } [MethodImpl(MethodImplOptions.Synchronized)] remove { delegateCache.Remove(logEventKey, value); } } protected void OnLog(string message) { LogHandler lh = (LogHandler) delegateCache.Find(logEventKey); if (lh != null) lh(message); } public void Process() { OnLog("Process() begin"); // other stuff here? OnLog("Process() end"); } } class Test { static void Logger(string s) { Console.WriteLine(s); } public static void Main() { MyClass myClass = new MyClass(); myClass.Log += new MyClass.LogHandler(Logger); myClass.Log += new MyClass.LogHandler(Logger); myClass.Process(); myClass.Log -= new MyClass.LogHandler(Logger); myClass.Process(); } }
DelegateCache 類別會為我們完成棘手的工作,而且可以讓其他類別用來儲存它們的委派。Add 和 Remove 存取子會用 Helper 類別來儲存委派。LogEventKey 變數用來產生這個事件獨有的索引鍵,但是不會佔用物件的空間,因為它是靜態的變數。然後在我們呼叫 DelegateCache 執行個體中的函式時,會使用這個索引鍵來確認我們得到的是正確的委派。這個範例輸出的程式碼如下:
Process() begin Process() begin Process() end Process() end Process() begin Process() end
我們也可以實作整個應用程式而非針對單一執行個體的快取。要這樣做,就必須根據執行個體和索引鍵來儲存委派。
酷站收集
這個月只有一個網站:
http://www.worldofdotnet.net
Eric Gunnerson 是 C# 編譯器小組的品管組長、C# 設計小組的成員及<A Programmer's Introduction to C#>的作者。他撰寫程式的時間已經長到足以知道 8 英吋的磁片是什麼,並且曾經能夠以單手掛上磁帶。