委派和事件
委派 (Delegate)
委派提供了安全的函數回呼(callback)機制(註一),這是寫.NET不可以不知道委派這個重要的機制。
在委派的內部,包含了一個指向某個方法的指標,在這一點上,委派的實作機制和C++函數指標完全相同,委派和其他所有.NET成員一樣,是一種類別,任何委派物件都是System.Delegate的某個衍生類別的一個物件。
一個簡單的委派
/// <summary>
/// 定義一個委派
/// </summary>
/// <remarks>
/// Delegate 定義要傳入的方法 ,無回傳值、傳入一個int參數
/// </remarks>
/// <param name="i">委派要傳入的參數</param>
private delegate void MyDelegate(int i);
static void Main(string[] args)
{
// 呼叫委派,並傳入method
MyDelegate d = new MyDelegate(PrintMessage);
//執行委派,傳入參數
// 寫法一:
d.Invoke(100); // output : i * i = 10000
// 寫法二:
d(100); // output : i * i = 10000
}
/// <summary>
/// 符合Delegate定義的方法的method
/// </summary>
/// <param name="i"></param>
static void PrintMessage(int i)
{
Console.WriteLine("i * i = " + i * i);
}
鏈式委派 (MulticastDelegate)
當兩個以上的委派被連結到一起而形成一個委派鏈時,呼叫開頭的委派,將使得該鏈上的所有委派方法都被執行,且預設會按照連結的先後順序執行。
/// <summary>
/// 定義一個委派
/// </summary>
/// <remarks>
/// Delegate 定義要傳入的方法 ,無回傳值
/// </remarks>
private delegate void MyDelegate();
static void Main(string[] args)
{
// 鏈式委派 寫法一
MyDelegate d = new MyDelegate(PrintMessage1);
d += new MyDelegate(PrintMessage2);
d += new MyDelegate(PrintMessage3);
d();
// output:
// 執行方法一
// 執行方法二
// 執行方法三
/*-----------------------------------------------------*/
// 鏈式委派 寫法二
MyDelegate handler1 = new MyDelegate(PrintMessage1);
MyDelegate handler2 = new MyDelegate(PrintMessage2);
MyDelegate handler3 = new MyDelegate(PrintMessage3);
MyDelegate handlerHead = handler1 + handler2 + handler3;
handlerHead();
// output:
// 執行方法一
// 執行方法二
// 執行方法三
}
private static void PrintMessage1()
{
Console.WriteLine("執行方法一");
}
private static void PrintMessage2()
{
Console.WriteLine("執行方法二");
}
private static void PrintMessage3()
{
Console.WriteLine("執行方法三");
}
定義有回傳值的委派
使用有回傳的的委派時,要注意如果只使用,上面所學的使用,則只會取得最後一筆回傳值
/// <summary>
/// 定義一個委派
/// </summary>
/// <remarks>
/// Delegate 定義要傳入的方法 ,回傳string
/// </remarks>
/// <returns>回傳字串</returns>
private delegate string MyDelegate();
static void Main(string[] args)
{
MyDelegate d = new MyDelegate(PrintMessage1);
d += new MyDelegate(PrintMessage2);
string message = d();
// 只取得最後一個回傳的字串
Console.WriteLine(message); // output: 訊息二
}
private static string PrintMessage1()
{
return "訊息一";
}
private static string PrintMessage2()
{
return "訊息二";
}
丟失大部分的回傳值可能會造成很大的麻煩,所以需要修改一些寫法來取得每一個回傳值
/// <summary>
/// 定義一個委派
/// </summary>
/// <remarks>
/// Delegate 定義要傳入的方法 ,回傳string
/// </remarks>
/// <returns>回傳字串</returns>
private delegate string MyDelegate();
private static void Main(string[] args)
{
MyDelegate d = new MyDelegate(PrintMessage1);
// 也可以不用new MyDelegate , 直接加方法
d += PrintMessage2;
foreach (Delegate item in d.GetInvocationList())
{
string message = item.DynamicInvoke().ToString();
Console.WriteLine(message);
// output1: 訊息一
// output2: 訊息二
}
}
private static string PrintMessage1()
{
return "訊息一";
}
private static string PrintMessage2()
{
return "訊息二";
}
委派是可以帶回傳值的,只是需要手動呼叫委派鏈上的每個方法,否則回傳值會遺失,只會取得最後一個方法的回傳值。
事件
在.NET中,事件和委派在本質上並沒有太多的差異,實際環境下,事件的運用比委派更加廣泛。
在微軟的產品文件上定義的事件:事件是一種物件或類別能夠提供通知的成員。使用端可以透過提供事件處理常式,為對應的事件加入可執行的程式碼。
在講事件的設計之前,先Demo一段普通的Delegate用法
public class SimpleDelegateForEvent
{
public delegate void FakeEvent(object sender, EventArgs e);
public void DomSomething()
{
FakeEvent f = new FakeEvent(Speak);
f(this, EventArgs.Empty);
}
private void Speak(object sender, EventArgs e)
{
Console.WriteLine("Wow!!");
}
}
static void Main(string[] args)
{
SimpleDelegateForEvent s = new SimpleDelegateForEvent();
s.DomSomething();
// 這行是沒辦法使用的
// 因為 delegate,無法從類別以外的地方新增方法
s.FakeEvent;
}
所以這邊我們將delegate改成event的方式,程式碼如下
public class SimpleDelegateForEvent
{
public event EventHandler MyEvent;
public void DomSomething()
{
if (MyEvent != null)
{
MyEvent(this, EventArgs.Empty);
}
}
}
static void Main(string[] args)
{
SimpleDelegateForEvent s = new SimpleDelegateForEvent();
// event 可以從類別以外的地方,加上方法
s.MyEvent += Speak;
s.DomSomething(); // output : Wow!!
}
private static void Speak(object sender, EventArgs e)
{
Console.WriteLine("Wow!!");
}
以上兩段程式碼,讀者應該很清楚能了解,delegate 與 event 的差異。
那實際上設計 event 時,有些步驟說明如下。
設計和使用事件的全部過程,須包含下列幾個步驟
-
定義一個衍生自System.EventArgs的參數類別(若沒有必要,可忽略)
-
在事件的管理類別中定義事件私有成員
-
通知事件訂閱者
-
事件使用上,用戶端可訂閱/取消事件
/// <summary>
/// 自訂一個事件參數類別
/// </summary>
public class EventArgsSample : EventArgs
{
private string _message;
/// <summary>
/// 建構式
/// </summary>
public EventArgsSample()
{
this._message = string.Empty;
}
/// <summary>
/// 建構式傳入訊息
/// </summary>
/// <param name="message">訊息</param>
public EventArgsSample(string message)
{
this._message = message;
}
/// <summary>
/// 訊息唯讀
/// </summary>
public string Message
{
get { return _message; }
}
}
/// <summary>
/// 管理主控台,在輸出前發送輸出事件
/// </summary>
public class ConsoleManager
{
// 定義事件成員物件
public event EventHandler<EventArgsSample> ConsoleEvent;
/// <summary>
/// 輸出
/// </summary>
/// <param name="message">用來傳給事件的訊息參數</param>
public void ConsoleOutput(string message)
{
//建立自訂事件參數類別
EventArgsSample args = new EventArgsSample(message);
// 呼叫發送事件Method
// 傳入自訂事件參數
SendConsoleEvent(args);
}
/// <summary>
/// 負責發送事件
/// </summary>
/// <param name="args">事件參數</param>
protected virtual void SendConsoleEvent(EventArgsSample args)
{
// 方法一: 呼叫定義事件成員物件
// ConsoleEvent(this, args);
// 方法二: 定義一個臨時的參考變數,這樣可以確保多執行緒呼叫時不會發生問題
EventHandler<EventArgsSample> temp = ConsoleEvent;
if (temp != null)
{
temp(this, args);
}
}
}
/// <summary>
/// 訂閱控制台輸出事件
/// </summary>
public class Log
{
public Log(ConsoleManager manager)
{
// 訂閱控制台輸出事件
// 使用方法跟委派一樣,加入方法
manager.ConsoleEvent += Output;
}
/// <summary>
/// 事件處理方法,輸出訊息
/// </summary>
/// <param name="send">事件發送者(ConsoleManager)</param>
/// <param name="args">自訂事件參數(EventArgsSample)</param>
private void Output(object send, EventArgs args)
{
string message = ((EventArgsSample)args).Message;
Console.WriteLine("事件處理方法:" + message);
}
}
上敘範例執行了先前列的四個步驟
-
定義事件參數類別:EventArgsSample
-
定義事件類型的私有成員:ConsoleManager.ConsoleEvent
-
定義事件通知方法:ConsoleManager.SendConsoleEvent
-
訂閱事件:Log類別
使用方法
static void Main(string[] args)
{
// 建立主控台,等待被其他類別訂閱
ConsoleManager cm = new ConsoleManager();
// Log 類別,訂閱事件
// 此時會將Log類別的private void Output方法,加到ConsoleManager類別的ConsoleEvent事件物件
Log log = new Log(cm);
// 事件訂閱完成
// 程式步驟說明
// 1.cm.ConsoleOutput只有負責做SendConsoleEvent這個方法 (發送事件)
// 2.SendConsoleEvent 負責執行事件成員物件 EventHandler<EventArgsSample> ConsoleEvent;
// 3.因為剛剛Log類別,已在 ConsoleEvent 加上 Log 類別的Output方法
// 4.所以當cm.ConsoleOutput 執行的時候,會去執行Log類別的Output方法
// 5.並且輸出 訊息
cm.ConsoleOutput("Hello"); // Output:事件處理方法:Hello
cm.ConsoleOutput("World"); // Output:事件處理方法:World
}
事件是一種特殊的委派,是一種讓物件或類別能夠提供通知的成員。使用端可以透過提供事件處理嘗常式(event handler),為對應的事件加入可執行的程式碼。
事件和委派的關係
事實上,事件本身就是一個委派型別,當定義一個事件時,是定義了一個特殊的委派成員,該委派沒有回傳值,並且擁有兩個參數:object sender 和 TEventArgs e。而當事件使用者訂閱事件時,本質上就是把事件處理方法加入到委派鏈中。
註一:
callback實際上的行為就是,建立一個MethodA,並將MethodA傳入MethodB,讓MethodB執行MethodA的概念
// 此程式碼為解釋callback概念,無法執行
//MethodA
public void MethodA(int a){};
//MethodB
public void MethodB(傳入MethodA)
{
// Do Something...
//執行傳入的MethodA();
MethodA(10);
}
一天一分享,身體好健康。
該追究的不是過去的原因,而是現在的目的。