C#的代理和事件
作者: Eric Gunnerson
代理 (Delegate)
大多數情況下,當調用函數時我們會指定要直接調用的函數。比如類 MyClass 如具有一個名為 Process 的函數,我們通常會按如下方法進行調用:
MyClass myClass = new MyClass();
myClass.Process();
這種調用在大多數情況下都是可行的。但是有些時候,我們不想直接調用函數,而希望能夠將它傳遞給其他人,讓他們進行調用。在以事件驅動的系統(如圖形用戶 界面)中,這種方法尤為有用。例如當我需要在用戶單擊某個按鈕即可執行一些代碼時,或者當我要記錄一些信息但卻無法指定記錄方式時。
考慮以下示例:
public class MyClass
{
public void Process()
{
Console.WriteLine("Process() begin");
// 這裡還有其他東西...
Console.WriteLine("Process() end");
}
}
在此類中,我們進行一些記錄,以了解函數的開始時間和結束時間。但是,我們的記錄僅限於發送到控制台,這可能不是我們所需要的。我們真正需要的是能夠控制從函數外部記錄信息的位置,同時不必使函數代碼變得復雜。
在這種情況下,代理便是理想的解決方法。代理使我們可以指定將要調用的函數,看起來好像不需要指定哪個函數一樣。對代理的聲明類似於對函數的聲明,不同的是在這種情況下,我們所聲明的是此代理可引用的函數簽名。
我們的例子將聲明一個帶有單個字符串參數且沒有返回類型的代理。修改該類如下:
public class MyClass
{
public delegate void LogHandler(string message);
public void Process(LogHandler logHandler)
{
if (logHandler != null)
logHandler("Process() begin");
// 這裡還有其他東西
if (logHandler != null)
logHandler ("Process() end");
}
}
使用代理與直接調用函數相似。只是在調用函數前,我們需要檢查代理是否為空(即不指向一個函數)。
要調用 Process() 函數,我們需要聲明一個與代理相匹配的記錄函數,然後創建代理的實例,以指向該函數。然後,將此代理傳遞給 Process() 函數。
class Test
{
static void Logger(string s)
{
&nb
$False$
sp; 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++ 語言,您可能會認為代理很像函數指針,這種想法非常接近於事實。但是,代理並不“僅僅”是函數指針,它還提供了其它多種功能。
傳遞狀態 (Passing State)
在上面的簡單示例中,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() 函數 -對代理來說代碼都是相同的,無論引用的是靜態函數還是成員函數,。
多播 (Multicasting)
雖然指向成員函數的功能已讓人感到滿意,但利用代理,您還可以巧妙地完成其它一些任務。在 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)
{
// 在這裡處理異常情況嗎?
}
}
代碼只是將每次調用包裝在一個 try-catch 對中,這樣在一個調用處理(handler)中引發的異常就不會妨礙對其它調用處理(handler)的激活。
事件 (Events)
我們已經對代理進行了較長時間的討論,現在該談一談事件了。一個顯而易見的問題就是:“既然我們已經有了代理,為什麼還需要事件?”
回答這個問題的最好方法就是考慮用戶界面對象所發生的事件。例如,一個按鈕可能有公共的“Click”代理。我們可將一個函數掛接到該代理上,這樣當單擊此按鈕時,就可以調用該代理。例如:
Button.Click = new Button.ClickHandler(ClickFunction);
它表示當單擊此按鈕時,將調用 ClickFunction()。
小測驗:上述代碼是否存在問題?我們忘記了什麼?
答案是,我們忘記使用 += 而直接分配了代理。這表示其它任何掛接到“Button.Click”的代理現在都將解除掛接。“Button.Click”應該是公共的,以便其它對象可以對其進行訪問,因此上述情況將無法避免。同樣,要刪除代理,用戶可能會編寫以下代碼:
Button.Click = null;
這將刪除所有代理。
這些情形極其糟糕,因為在許多情況下只掛接了一個代理,問題不會明顯地表現為bug。隨後,當掛接了另一個代理時,事情就糟了!
事件在代理模型上添加了一層保護。這裡有一個支持事件的對象例子:
public class MyObject
{
public delegate void ClickHandler(object sender, EventArgs e);
public event ClickHandler Click;
protected void OnClick()
{
if (Click != null)
Click(this, null);
}
}
ClickHandler 代理使用事件代理的標准模式來定義事件的簽名。它以handler的名字結尾,帶有兩個參數。第一個參數是發送此事件的對象,第二個參數用於傳遞伴隨事件發生的信息。本例中沒有要傳遞的信息,因此直接使用 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);
}
}我們創建一個與代理簽名相匹配的靜態函數或成員函數,然後用 += 向事件中添加代理的一個新實例。