談談委派 (Delegate)

一點回顧

上個月我們談到效能和 Box 動作。讀者提出的問題之一是,為什麼 Perl 版會比 C# 版快那麼多。答案之一是 Perl 非常擅長做這種事情。有一位讀者指出,我忘了與其在每一個符合的項目上都加以解譯,不如讓 Regex 來編譯規則運算式 (Regular Expression)。在 Beta 1 中,解決方法是傳遞「c」做為 Regex 建構函式的第二個參數 (在 Beta 2 中則用列舉來做同樣的事情)。這樣可以將成本減少幾乎一半,並將最快的版本所需的時間降低到略低於 7 秒鐘。

委派和事件

本月專欄的主題是委派 (Delegate) 和事件。呃,其實只有委派而已,因為事件是下個月的主題。我會先解釋委派是什麼、能做什麼、以及如何使用,然後下個月再來談事件。

委派

在大部分情況中,我們在呼叫函式時,都會直接指定要呼叫的函式。如果 MyClass 類別擁有一個叫做 Process 的函式,我們通常會以下列方式呼叫它:

MyClass myClass = new MyClass();
myClass.Process();

這在大部分情況下都行得通。不過,有時候我們不想直接呼叫函式—而希望能夠將它傳遞給別人,供他們呼叫。這在事件驅動系統中尤其有用,例如在圖形使用者介面中,我希望某些程式碼在使用者按一下按鈕時被執行、或者我要記錄某些資訊,但是卻無法指定如何記錄它時。

請參考下列範例:

public class MyClass
{
   
public void Process()
   
{
      Console.WriteLine(
"Process() begin");
      
// other stuff here?
      Console.WriteLine("Process() end");
   }

}

在這個類別中,我們正在做一些記錄,以便知道函式何時開始和結束。不幸的是,我們的記錄只能硬接到主控台,而這可能並不是我們要的。我們真正想要的是,有辦法控制資訊要從函式外的哪裡記錄,而不必讓函式程式碼變得更複雜。

委派正好適合這個用途,它可以讓我們指定要呼叫的函式的模樣,而不必指定要呼叫哪一個函式。委派的宣告看起來就像是函式的宣告,只不過在這個案例中,我們宣告的是這個委派可以參考的函式的簽名碼 (Signature)。

就我們的範例來說,我們要宣告接受單一字串參數而且沒有傳回型別的委派。我們的類別修改如下:

public class MyClass
{
   
public delegate void LogHandler(string message);
   
public void Process(LogHandler logHandler)
   
{
      
if (logHandler != null)
         logHandler(
"Process() begin");
      
// other stuff here?
      if (logHandler != null)
         logHandler (
"Process() end");
   }

}

使用委派就像直接呼叫函式一樣,不過,在呼叫函式之前,我們需要加上一項檢查,看看委派是否為 Null (也就是說,沒有指向函式)。

如果要呼叫 Process() 函式,我們需要宣告與委派相符的記錄函式,再建立指向函式的委派的執行個體。然後這個委派會被傳遞給 Process() 函式。

class Test
{
   
static void Logger(string s)

   
{
      Console.WriteLine(s);
   }


   
public static void Main()
   
{
      MyClass myClass 
= new MyClass();

      MyClass.LogHandler lh 
= new MyClass.LogHandler(Logger);
   
      myClass.Process(lh);
   }

}

Logger() 函式就是我們想要從 Process() 函式呼叫的函式,而且它會被宣告以便符合委派。我們在 Main() 中建立委派的執行個體,並將委派建構函式傳遞給我們要建構函式指向的這個函式。最後將委派傳遞給 Process() 函式,才能夠呼叫 Logger() 函式。

如果您是 C++ 的使用者,可能會認為委派很像函式指標,您的想法算是很正確。不過,委派不只是函式指標而已,它還提供其他功能。

傳遞狀態

在上面的簡單範例中,Logger() 函式只有寫出字串。不同的函式可能會想將資訊記錄到檔案中,不過,要這樣做,函式必要知道要將資訊寫入那個檔案。

於 Win32® 中,在傳遞函式指標時,您可以將狀態一起傳遞出去。不過,在 C# 中就沒有必要這樣做,因為委派可以參考靜態成員函式。底下是如何參考成員函式的範例:

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
{
   
public static void Main()
   
{
      FileLogger fl 
= new FileLogger("process.log");
      
      MyClass myClass 
= new MyClass();

      MyClass.LogHandler lh 
= new MyClass.LogHandler(fl.Logger);
   
      myClass.Process(lh);
      fl.Close();
   }

}

FileLogger 類別只會封裝檔案。修改 Main() 才能讓委派指向 FileLogger 的 fl 執行個體上的 Logger() 函式。在從 Process() 叫用這個委派時,就會呼叫成員函式,並將字串記錄到適當的檔案中。

酷的是我們不必變更 Process() 函式;不論是否參考靜態或成員函式,所有委派的程式碼都是一樣的。

多點傳送

能夠指向成員函式固然好,但是用委派做的還不止於此。在 C# 中,委派是多點傳送的,這表示它們可以同時指向一個以上的函式 (也就是說,它們不以 System.MulticastDelegate 型別為基礎)。多點傳送委派有一份函式清單,當委派被叫用時,清單中的所有函式也都會被呼叫。我們可以在第一個範例的記錄函式中加回去,並呼叫兩個委派。為了合併兩個委派,我們要使用 Delegate.Combine() 函式。程式碼如下所示:

      MyClass.LogHandler lh = (MyClass.LogHandler) 
         Delegate.Combine(
new Delegate[] 
            
{new MyClass.LogHandler(Logger),
            
new MyClass.LogHandler(fl.Logger)}
);

哇,真是難看!與其強迫使用者接受這個語法,不如由 C# 提供更好的語法。您可以不要呼叫 Delegate.Combine(),而只要用 += 將兩個委派合併起來即可:

      MyClass.LogHandler lh = null;
      lh 
+= new MyClass.LogHandler(Logger);
      lh 
+= new MyClass.LogHandler(fl.Logger);

這樣就清爽多了。如果要將委派從多點傳送委派中移除,可以呼叫 Delegate.Remove(),或者使用 -= 運算子 (我知道我會用哪一個)。

呼叫多點傳送委派時,叫用清單中的委派會依它們出現的順序被同步呼叫。如果處理序期間發生錯誤,執行處理序會中斷。

如果想更密切的控制叫用順序—例如,如果您想要有保證的叫用—可以從委派取得叫用清單,並自行叫用函式。方法如下所示:

foreach (LogHandler logHandler in lh.GetInvocationList())
{
   
try
   
{
      logHandler(message);
   }

   
catch (Exception e)
   
{
      
// do something with the exception here?
   }

}

程式碼在 Try-Catch 中只會包裝各項呼叫,所以處理常式中擲回的例外狀況 (Exception) 並不會妨礙其他處理常式被呼叫。

事件

我們已經談了不少委派,現在該來談談事件了。一個顯而易見的問題是:「既然已經有了委派,為什麼還要事件?」

最好的回答就是,研究一下使用者介面物件的案例。例如,按鈕上面可能有一個公用的 Click 委派。我們可以將函式連結到該委派,那樣在按下按鈕時就會呼叫委派。我們可以這樣做:

   Button.Click = new Button.ClickHandler(ClickFunction);

這表示在按下按鈕時,就會呼叫 ClickFunction()

問題:上面的程式碼有問題嗎?我們忘了什麼?

答案是我們忘了使用 +=,而直接指定了委派。這表示任何連結到 Button.Click 的其他委派都會被解除攔截。Button.Click 必須是公用 (Public) 的,其他物件才能存取它,而我們無法阻止上述的情況。同樣的,如果要移除委派,使用者可以這樣寫:

   Button.Click = null;

這樣會移除所有委派。

這些情況特別糟糕,因為在很多案例中,都只連結一個委派,問題不會明顯到被當成錯誤。若日後連結了另一個委派,問題就開始惡化了。

事件會在委派模型上方加一層保護。底下是可支援事件的物件之範例:

public class MyObject
{
   
public delegate void ClickHandler(object sender, EventArgs e);
   
public event ClickHandler Click;

   
protected void OnClick()
   
{
      
if (Click != null)
         Click(
thisnull);
   }

}

ClickHandler 委派會用事件委派的標準模式來定義事件的簽名碼。它的名稱以處理常式結尾,而且有兩個參數。第一個參數是傳送物件的物件,第二個參數則用來傳遞和事件一起發出的資訊。這個案例沒有要傳遞的資訊,所以直接使用 EventArgs,但是有資料要傳遞時,就要使用從 EventArgs 衍生的類別 (例如 MouseEventArgs)。

宣告 Click 事件要做兩件事。首先,宣告名為 Click 的委派成員變數,這個變數會從類別內部被使用。其次,宣告名為 Click 的事件,這個事件可以根據一般的存取規則,從類別外部被使用 (在這個案例中,事件是公用的)。

通常都會包括一個像 OnClick() 的函式,使得型別或衍生型別可以引發事件,您會發現,它用來引發事件的程式碼和委派的程式碼一樣,因為 Click 是一個委派。

就像委派一樣,我們攔截和取消攔截使用 +=-= 的事件,和委派不同的地方,在於這些是唯一能在事件上執行的作業。這樣可以確保先前談到的兩項錯誤不會發生。

事件的使用方式簡單明瞭。

class Test
{
   
static void ClickFunction(object sender, EventArgs args)
   
{
      
// process the event here.
   }

   
public static void Main()
   
{
      MyObject myObject 
= new MyObject();

      myObject.Click 
+= new MyObject.ClickHandler(ClickFunction);      
   }

}

我們建立和委派的簽名碼相符的靜態函式或成員函式,然後用 += 將委派的新執行個體加入事件。

本月到此為止。下個月我們要談一下事件如何在內部運作,以及實作事件的一些進階方法 (提示:事件有時候因為有額外保護而略不同於委派)。如果您真的好奇地等不及了,範例程式碼裡也有下個月的程式碼。

酷站收集

很不幸,這個月我的手提電腦有問題,傳送給我的網站都遺失了。如果您有傳送 URL 給我,卻還沒有在網站上看到,請重傳到 ericgu@microsoft.com 給我。

posted @ 2004-10-24 21:48  Benny Ng  阅读(3389)  评论(0编辑  收藏  举报