Delegate and Event

委派和事件

委派 (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 時,有些步驟說明如下。

 

設計和使用事件的全部過程,須包含下列幾個步驟
  1. 定義一個衍生自System.EventArgs的參數類別(若沒有必要,可忽略)
  2. 在事件的管理類別中定義事件私有成員
  3. 通知事件訂閱者
  4. 事件使用上,用戶端可訂閱/取消事件
/// <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);
	}
}
上敘範例執行了先前列的四個步驟
  1. 定義事件參數類別:EventArgsSample
  2. 定義事件類型的私有成員:ConsoleManager.ConsoleEvent
  3. 定義事件通知方法:ConsoleManager.SendConsoleEvent
  4. 訂閱事件: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);
}

 

 

 

一天一分享,身體好健康。

該追究的不是過去的原因,而是現在的目的。