深入探討事件 (Event)

使用 C#  

深入探討事件 (Event)

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 英吋的磁片是什麼,並且曾經能夠以單手掛上磁帶。

posted @ 2004-10-24 23:01  Benny Ng  阅读(828)  评论(0编辑  收藏  举报