C#/.NET中的委託與事件
--------------------------------------------------------------------------------
目錄
譯者的話
概述
委託(Delegates)
直接調用方法-不用委託
最基本的委託
調用靜態方法
調用成員方法
多路廣播
事件(Events)
慣例
一個簡單事件的樣本
第二個事件例子
結論
--------------------------------------------------------------------------------
譯者的話(By LuBen)
委託和事件對於初學者來說,總是難以理解。以前看到過關於委託方面的文章.NET Delegates: A C# Bedtime Story,寫的非常好,網上也有其中文版本。但是對於初學者來說,下面這篇文章似乎更加通俗易懂,所以特別翻譯一下,供初學者學習。 英文原文地址:Delegates and Events in C# / .NET
概述
今天,各種各樣事件驅動(event driven)的編程方式充斥著我們的視野。C# 通過支援事件與委託(events and delegates) 給事件驅動編程世界注入了新的活力。本文重點探討當我們給普通UI控制項添加一個事件處理常式(event handle)時到底發生了什麼。 通過一個簡單的類比,給Button類添加AddOnClick或類似事件,我們將探尋發生在幕後的真實故事。這將協助我們更好的理解使用了多路廣播委託(multi cast delegates)的事件處理常式的本質:)
Delegates(委託)
C#中的委託類似於C或者C++中的函數指標。利用委託,程式員可以在委派物件中封裝一個方法的引用。 然後委派物件將被傳給調用了被引用方法的代碼,而不需要知道在編譯時間刻具體是哪個方法被調用。(譯者註:如果您不理解這段話,先看下去回過頭再來理解)
直接調用方法-不用委託
在大多數情況下,當我們調用一個方法時,我們是直接指定調用的方法。 例如,如果類MyClass有個名為Process的方法,我們通常會這樣調用它 (SimpleSample.cs):
using System;
namespace Akadia.NoDelegate
{
public class MyClass
{
public void Process()
{
Console.WriteLine("Process() begin");
Console.WriteLine("Process() end");
}
}
public class Test
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
myClass.Process();
}
}
}
在大多數情況下,這樣子做就足夠了。但是,有些時候,我們並不想去直接調用方法 - 我們想把它傳給某個其他的對象,讓其能夠調用它。這個在事件驅動的系統中非常有用的, 象GUI(graphical user interface),當我們想在使用者點擊按鈕時執行某段代碼,或者是當我們想記錄日誌,但是又不知道該怎麼去記錄時。
最基本的委託
委託一個很有趣又有用的特性是它並不知道或者說是關心它引用的方法的類別。 任何方法都可以,只要這個方法的參數類型和傳回值類型能夠匹配它。這個特性使得委託十分適合匿名調用("anonymous" invocation)。
單路廣播委託型構(signature)如下:
delegate result-type identifier ([parameters]);
這裡:
result-type: 傳回值的類型,和方法的傳回值類型一致
identifier: 委託的名稱
parameters: 參數,要引用的方法帶的參數
例:
public delegate void SimpleDelegate ()
這個聲明定義了一個名為SimpleDelegate的委託,它可以封裝任何不帶參數不傳回值的方法。
public delegate int ButtonClickHandler (object obj1, object obj2)
這個聲明定義了一個名為ButtonClickHandler的委託,它可以封裝任何帶2個Objec參數返回int類型值的方法。
委託允許我們只需指定調用的方法是什麼樣子,而不需要指定具體調用哪個方法。委託的聲明看起來就像是方法的聲明,除了一個情形,那就是在我們要聲明的方法正好是這個委託能夠引用的方法的時候。
定義和使用委託有三個步驟:
聲明
執行個體化
調用
一個很基礎的樣本(SimpleDelegate1.cs):
using System;
namespace Akadia.BasicDelegate
{
// 聲明
public delegate void SimpleDelegate();
class TestDelegate
{
public static void MyFunc()
{
Console.WriteLine("I was called by delegate ...");
}
public static void Main()
{
// 執行個體化
SimpleDelegate simpleDelegate = new SimpleDelegate(MyFunc);
// 調用
simpleDelegate();
}
}
}
編譯測試:
# csc SimpleDelegate1.cs
# SimpleDelegate1.exe
I was called by delegate ...
調用靜態方法
下面是一個複雜點的例子(SimpleDelegate2.cs), 聲明了一個帶一個string型別參數不傳回值的委託:
using System;
namespace Akadia.SimpleDelegate
{
// 委託定義
public class MyClass
{
// 聲明一個帶string參數不傳回值的委託
public delegate void LogHandler(string message);
// 使用委託就像使用方法一樣。不過我們在調用前需要檢查委託是否為空白(委託沒有引用任何方法)
public void Process(LogHandler logHandler)
{
if (logHandler != null)
{
logHandler("Process() begin");
}
if (logHandler != null)
{
logHandler ("Process() end");
}
}
}
// 測試應用程式,使用上面定義的委託
public class TestApplication
{
// 靜態方法:將被委託引用。為了調用Process()方法,
// 我們定義一個記錄日誌的方法:和委託型構匹配的Logger()
static void Logger(string s)
{
Console.WriteLine(s);
}
static void Main(string[] args)
{
MyClass myClass = new MyClass();
// 創造委託執行個體,引用上面定義的日誌方法。該委託將被傳給Process()方法。
MyClass.LogHandler myLogger = new MyClass.LogHandler(Logger);
myClass.Process(myLogger);
}
}
}
編譯測試:
# csc SimpleDelegate2.cs
# SimpleDelegate2.exe
Process() begin
Process() end
調用成員方法
在上面簡單的例子中,方法Logger( ) 僅僅實現寫出字串。我們可能需要一個不同的方法把把日誌資訊寫入到檔案中,如果要實現如此,則這個方法需要知道把日誌資訊寫入到哪一個檔案(SimpleDelegate3.cs):
using System;
using System.IO;
namespace Akadia.SimpleDelegate
{
// 委託的定義
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");
}
}
}
// 封裝檔案I/O操作的類
public 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();
}
}
// 委託指向FileLogger類執行個體f1的Logger()方法。
// 當委託在Process()被調用時,成員方法Logger()也將被調用,日誌被寫入到指定的檔案中。
public class TestApplication
{
static void Main(string[] args)
{
FileLogger fl = new FileLogger("process.log");
MyClass myClass = new MyClass();
// 創造委託執行個體,引用上面定義的日誌方法。該委託將被傳給Process()方法。
MyClass.LogHandler myLogger = new MyClass.LogHandler(fl.Logger);
myClass.Process(myLogger);
fl.Close();
}
}
}
這部分很酷的一點是我們並不需要改變Process()方法,不管委託引用的是static還是member方法,委託定義調用部分代碼都一樣。
測試編譯:
# csc SimpleDelegate3.cs
# SimpleDelegate3.exe
# cat process.log
Process() begin
Process() end
多路廣播(Multicasting)
能夠引用成員方法已經很不錯了,但是運用委託我們還可以做到更多。 在C#中,委託是多路廣播的(multicast),也就是說它們可以一次同時指向多個方法。多路廣播委託維護著一個方法列表,這些方法在該委託被調用時都將被調用。
using System;
using System.IO;
namespace Akadia.SimpleDelegate
{
// 委託的定義
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");
}
}
}
// 封裝檔案I/O操作的類
public class FileLogger
{
FileStream fileStream;
StreamWriter streamWriter;
// Constructor
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();
}
}
// 調用多個委託的測試應用程式
public class TestApplication
{
// 委託中將要使用的靜態方法
static void Logger(string s)
{
Console.WriteLine(s);
}
static void Main(string[] args)
{
FileLogger fl = new FileLogger("process.log");
MyClass myClass = new MyClass();
// 創造一個委託執行個體,引用TestApplication
// 中定義的靜態方法Logger()和FileLogger類的執行個體f1的成員方法。
MyClass.LogHandler myLogger = null;
myLogger += new MyClass.LogHandler(Logger);
myLogger += new MyClass.LogHandler(fl.Logger);
myClass.Process(myLogger);
fl.Close();
}
}
}
測試編譯:
# csc SimpleDelegate4.cs
# SimpleDelegate4.exe
Process() begin
Process() end
# cat process.log
Process() begin
Process() end
Events(事件)
C#中的事件模型是以事件編程模型為基礎的,事件編程模型在非同步編程時非常普遍。這種編程模型源於 “出版者和訂閱者(publisher and subscribers)”思想。在這個模型中,出版者(subscribers)處理一些邏輯發布一個“事件”,它們僅發布這些的事件給訂閱了該事件的訂閱者(publishers)。
在C#中,任何對象都能發布一組其他應用程式能夠訂閱的事件。 當這些發布類(發布這些事件的類)觸發了該事件時,所有訂閱了該事件的應用程式都將被通知到。下面這副圖展現了這個機制:
慣例
下面是關於使用事件的一些重要慣例:
.NET Framework中的Event Handlers不返回任何值,帶有2個參數
第一個參數是事件的源,也就是發布該事件的對象(譯者註:具體來講,應該是發布事件的類的執行個體)
第二個參數是一個繼承了EventArgs的對象
事件作為發布它的類的屬性存在。
關鍵字event(事件)控制著事件訂閱類如何訪問該事件屬性
譯者註:EventHandler是.NET Framework內建的事件委託類型。如上所敘,它帶有2個參數,如果發布事件的類沒有資料需要傳送給訂閱類,那麼使用系統內建的EventHandler委託就足夠了。如果類似事件第二個例子一樣,需要把Clock資料傳給訂閱類,則通常使用自訂的委託,包含一個繼承EventArgs的參數類,讓這個參數類封裝要傳送的資料(如樣本2)。
一個簡單事件的樣本
下面我們不用委託,而是使用事件來修改實現上面的日誌例子
using System;
using System.IO;
namespace Akadia.SimpleEvent
{
/* ========= Publisher of the Event ============== */
public class MyClass
{
// 定義一個名為LogHandler的委託,它封裝帶有一個string參數不返回任何值的方法
public delegate void LogHandler(string message);
// 定義基於上面定義的委託的事件
public event LogHandler Log;
// 代替使用委託作為參數的Process()方法。
// 呼叫事件,使用OnXXXX方法,XXXX是事件的名稱
public void Process()
{
OnLog("Process() begin");
OnLog("Process() end");
}
// 防止事件為空白,建立OnXXXX方法呼叫事件
protected void OnLog(string message)
{
if (Log != null)
{
Log(message);
}
}
}
// 封裝檔案I/O操作的類
public 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();
}
}
/* ========= Subscriber of the Event ============== */
// 現在添加委託執行個體給事件變得更加簡單清晰
public class TestApplication
{
static void Logger(string s)
{
Console.WriteLine(s);
}
static void Main(string[] args)
{
FileLogger fl = new FileLogger("process.log");
MyClass myClass = new MyClass();
// 訂閱Logger方法和f1.Logger
myClass.Log += new MyClass.LogHandler(Logger);
myClass.Log += new MyClass.LogHandler(fl.Logger);
// 觸發Process()方法
myClass.Process();
fl.Close();
}
}
}
編譯測試:
# csc SimpleEvent.cs
# SimpleEvent.exe
Process() begin
Process() end
# cat process.log
Process() begin
Process() end
第二個事件例子
假設我們要建立一個Clock類,當本地時間每變化一秒鐘,該類就使用事件來通知潛在的訂閱者。 請看樣本:
using System;
using System.Threading;
namespace SecondChangeEvent
{
/* ======================= Event Publisher =============================== */
// 被其他類觀察的鐘(Clock)類,改類發布一個事件:SecondChange。觀察該類的類訂閱了該事件。
public class Clock
{
// 代表小時,分鐘,秒的私人變數
private int _hour;
private int _minute;
private int _second;
// 定義名為SecondChangeHandler的委託,封裝不傳回值的方法,
// 該方法帶參數,一個clock類型對象參數,一個TimeInfoEventArgs類型對象
public delegate void SecondChangeHandler (
object clock,
TimeInfoEventArgs timeInformation
);
// 要發布的事件
public event SecondChangeHandler SecondChange;
// 觸發事件的方法
protected void OnSecondChange(
object clock,
TimeInfoEventArgs timeInformation
)
{
// Check if there are any Subscribers
if (SecondChange != null)
{
// Call the Event
SecondChange(clock,timeInformation);
}
}
// 讓鐘(Clock)跑起來,每隔一秒鐘觸發一次事件
public void Run()
{
for(;;)
{
// 讓線程Sleep一秒鐘
Thread.Sleep(1000);
// 擷取目前時間
System.DateTime dt = System.DateTime.Now;
// 如果秒鐘變化了通知訂閱者
if (dt.Second != _second)
{
// 創造TimeInfoEventArgs類型對象,傳給訂閱者
TimeInfoEventArgs timeInformation =
new TimeInfoEventArgs(
dt.Hour,dt.Minute,dt.Second);
// 通知訂閱者
OnSecondChange (this,timeInformation);
}
// 更新狀態資訊
_second = dt.Second;
_minute = dt.Minute;
_hour = dt.Hour;
}
}
}
// 該類用來儲存關於事件的有效資訊外,
// 還用來儲存額外的需要傳給訂閱者的Clock狀態資訊
public class TimeInfoEventArgs : EventArgs
{
public TimeInfoEventArgs(int hour, int minute, int second)
{
this.hour = hour;
this.minute = minute;
this.second = second;
}
public readonly int hour;
public readonly int minute;
public readonly int second;
}
/* ======================= Event Subscribers =============================== */
// 一個訂閱者。DisplayClock訂閱了clock類的事件。它的工作是顯示當前事件。
public class DisplayClock
{
// 傳入一個clock對象,訂閱其SecondChangeHandler事件
public void Subscribe(Clock theClock)
{
theClock.SecondChange +=
new Clock.SecondChangeHandler(TimeHasChanged);
}
// 實現了委託匹配類型的方法
public void TimeHasChanged(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Current Time: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
// 第二個訂閱者,他的工作是把目前時間寫入一個檔案
public class LogClock
{
public void Subscribe(Clock theClock)
{
theClock.SecondChange +=
new Clock.SecondChangeHandler(WriteLogEntry);
}
// 這個方法本來應該是把資訊寫入一個檔案中
// 這裡我們用把資訊輸出控制台代替
public void WriteLogEntry(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Logging to file: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
/* ======================= Test Application =============================== */
// 測試擁有程式
public class Test
{
public static void Main()
{
// 建立clock執行個體
Clock theClock = new Clock();
// 建立一個DisplayClock執行個體,讓其訂閱上面建立的clock的事件
DisplayClock dc = new DisplayClock();
dc.Subscribe(theClock);
// 建立一個LogClock執行個體,讓其訂閱上面建立的clock的事件
LogClock lc = new LogClock();
lc.Subscribe(theClock);
// 讓鐘跑起來
theClock.Run();
}
}
}
結論
最後一個例子中的Clock類能夠簡單的實現列印時間而不是觸發事件,所以為什麼對關於使用委託的介紹感到煩躁呢?使用發布/訂閱模式的最大好處就是當一個事件觸發時能夠通知任意數目的訂閱類。這些訂閱類不需要知道Clock類如何工作,而這個Clock類也不需要知道訂閱者們如何來響應這個事件。類似的,按鈕能夠發布一個OnClick事件,任意數目不想關的對象能夠訂閱這個事件,並且在這個按鈕被點擊時被通知到。
發行者和訂閱者通過委託很好的實現瞭解耦。這樣大大增強了可擴充性和健壯性。 Clock類能夠改變其洞察時間的方式而不會干擾到那些訂閱類,訂閱類也能改變其對時間改變事件作出的反應而不用打擾Clock類。兩個類相互獨立,互不干擾,使得維護代碼變得更加容易。