委派演義(5) -- 委派與事件(二)

在這系列第一篇講到張無忌的時候曾經說到:『委派可以想像成方法(函式)的外號。』,不過看看將方法委派給事件的程式碼,好像有點怪怪的,它的外號哪裡去了?

       在這系列第一篇講到張無忌的時候曾經說到:『委派可以想像成方法(函式)的外號。』,不過看看將方法委派給事件的程式碼,好像有點怪怪的,它的外號哪裡去了?

 

       當然有一種很標準的講法叫做:『編譯器幫你做完這件事了。』;或是另一種講法:『事件是方法的一種特殊型式。』,這兩句話沒有錯,不過還是可以更深入瞭解事件引發後是怎麼執行其委派方法們 (是『們』,你沒看錯,的確是可以將多個方法委派給一個事件,還記得桃谷六仙嗎?當然另一方面你也可以將一個方法委派給多個事件),這樣感覺好像更有趣一點。

 

       各位觀眾,現在我們將鏡頭轉向和主題沒什麼關係的『屬性 (Property)』。在史前時代要定義一個屬性還滿囉唆的,必須要先宣告一個內部成員欄位來給屬性存取,如以下的程式碼:


	Private myPropertyValue As Boolean
	Public Property MyProperty As Boolean
		Get
			Return myPropertyValue
		End Get
		Set(value As Boolean)
			myPropertyValue = value
		End Set
	End Property

 


		private Boolean myProperty;
		public Boolean MyProperty
		{
			get { return myProperty; }
			set { myProperty = value; }
		}

 

       後來在 C# 3.0 上出現了一個新玩意叫『自動實作的屬性 (Auto-Implemented Properties) 』,於是可以這麼寫:


		public Boolean MyProperty
		{
			get;
			set;
		}

 

       再更久以後 Visual Basic 也跟進了 ( 自動實作的屬性 (Visual Basic) ),所以在 Visual Basic 10.0 (Visual Basic 2010) 變成可以這樣寫:


Public Property MyProperty As Boolean

 

       這和事件有什麼關係?有沒有覺得在前一篇文章中的事件宣告就像是自動實作屬性一樣便利,你只要正確宣告就好,如自動實作屬性一般,其它的麻煩事編譯器都會幫你搞定;但事件有沒有很囉唆如史前時代屬性一般的寫法,答案是有的,而且這個囉唆的寫法恰巧解釋了事件委派函式是怎麼在事件被觸發後執行的。

 

       每個事件的心中都有一份委派清單

       其實以下這玩意的正式名稱應該叫自訂事件 (Custom Event),以下來看看在 Visual Basic 和 C# 中如何實作自訂事件。

       Visual Basic

       (1) 在 Visual Basic 8.0 版 (也就是俗稱的 Visual Basic 2005) 時加入了一個 [ Custom 關鍵字 ] 用來宣告自訂事件,來改編一下前一篇程式碼:


Public Class EventSample
	Public Delegate Sub TestValueHandler(sender As EventSample, value As Boolean)

	Private delegateList As New List(Of TestValueHandler)
	Public Custom Event TestValueChanged As TestValueHandler
		AddHandler(value As TestValueHandler)
			SyncLock delegateList
				delegateList.Add(value)
			End SyncLock
		End AddHandler

		RemoveHandler(value As TestValueHandler)
			SyncLock (delegateList)
				delegateList.Remove(value)
			End SyncLock
		End RemoveHandler

		RaiseEvent(sender As Object, value As Boolean)
			SyncLock (delegateList)
				For Each handler As TestValueHandler In delegateList
					handler(sender, value)
					''你也可以這樣寫
					''handler.Invoke(sender, value)
				Next
			End SyncLock
		End RaiseEvent
	End Event

	Public WriteOnly Property TestValue As Boolean
		Set(value As Boolean)
			OnTestValueChanged(value)
		End Set
	End Property

	Public Property Name As String

	Protected Overridable Sub OnTestValueChanged(value As Boolean)
		RaiseEvent TestValueChanged(Me, value)
	End Sub

End Class

       看起來真豐富,多出一大堆程式碼,當你用 Custom 宣告一個自訂事件的時候,你會發現多出一堆東西,而且形式和屬性有點像:


	Public Custom Event TestValueChanged As TestValueHandler
		AddHandler(value As TestValueHandler)

		End AddHandler

		RemoveHandler(value As TestValueHandler)

		End RemoveHandler

		RaiseEvent()

		End RaiseEvent
	End Event

 

       AddHandler 區塊的內容即是當你將一個方法委派給事件的時候會執行的程式碼區塊,而那個參數就是在程式中用 AddressOf 所取得的方法參考;其實 AddressOf 方法名稱,就會以其方法簽章產生一個委派執行個體,然後以參考型別變數的型式傳進來。RemoveHandler 區塊的程式當然就是當你在程式中把委派方法移除的時候發生的。至於 RaiseEvent 區塊,則是當你使用 RaiseEvent 陳述式觸發事件時所會執行的程式碼區塊。(註:在這個 Custom Event 區塊中的 AddHandler 、 RemoveHandler 與 RaiseEvent 被稱為 『存取子』,切勿和同名的『陳述式』弄混了。)

 

       回歸到那個很豐富的程式碼,其中宣告了一個 List(Of TestValueHandler) 物件,其對應變數名稱為 delegateList,用來儲存加入到這個事件的委派,然後在 RaiseEvent 區塊中就可以藉由這個 List 來執行所加入的委派方法。江湖一點訣,說穿不值錢,一點都不難瞭解吧。

 

       (2) 當然你不一定要用一個 List(Of T) 來做,記得桃谷六仙嗎?委派是可以多點的,所以可以直接宣告一個 TestValueHandler 型別的委派變數來處理:


	Public Delegate Sub TestValueHandler(sender As EventSample, value As Boolean)

	Private TestValueChangedDelegate As TestValueHandler
	Public Custom Event TestValueChanged As TestValueHandler
		AddHandler(value As TestValueHandler)
			If TestValueChangedDelegate Is Nothing Then
				TestValueChangedDelegate = [Delegate].Combine(TestValueChangedDelegate, value)
			Else
				SyncLock (TestValueChangedDelegate)
					TestValueChangedDelegate = [Delegate].Combine(TestValueChangedDelegate, value)
				End SyncLock
			End If

		End AddHandler


		RemoveHandler(value As TestValueHandler)
			If Not TestValueChangedDelegate Is Nothing Then
				SyncLock (TestValueChangedDelegate)
					TestValueChangedDelegate = [Delegate].Remove(TestValueChangedDelegate, value)
				End SyncLock
			End If
		End RemoveHandler

		RaiseEvent(sender As Object, value As Boolean)
			If Not TestValueChangedDelegate Is Nothing Then
				SyncLock (TestValueChangedDelegate)
					For Each handler As TestValueHandler In TestValueChangedDelegate.GetInvocationList()
						handler(sender, value)
						''你也可以這樣寫
						''handler.Invoke(sender, value)
					Next
				End SyncLock
			End If

		End RaiseEvent
	End Event

 

       上頭這種用法使用一個很重要的 Method [ MulticastDelegate.GetInvocationList 方法 ] ,這個方法暫時先簡單解釋就是『依照引動過程的順序,傳回這個多點傳送委派的引動過程清單』,如果有機會的話,未來應該會特別為這個類別寫篇簡單的介紹。

 

       然後來看看在 Form 上的程式是如何使用這個類別與其事件,在程式碼中委派了兩個方法給同一個事件:


Public Class Form1

	Private sample As EventSample

	Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
		sample = New EventSample()
		sample.name = "範例物件"
		AddHandler sample.TestValueChanged, AddressOf sample_TestValueChanged00
		AddHandler sample.TestValueChanged, AddressOf sample_TestValueChanged01
	End Sub

	Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
		sample.TestValue = True
	End Sub

	Private Sub Button2_Click(sender As System.Object, e As System.EventArgs) Handles Button2.Click
		sample.TestValue = False
	End Sub

	Private Sub sample_TestValueChanged00(sender As EventSample, value As Boolean)
		MessageBox.Show(String.Format("引發事件的物件名稱:{0}", sender.name))
	End Sub

	Private Sub sample_TestValueChanged01(sender As EventSample, value As Boolean)
		MessageBox.Show(String.Format("測試值為:{0}", value))
	End Sub

End Class

 

       執行後就可以發現方法執行的順序和我們將其加入事件順序是相同的,因為在定義事件的 RaiseEvent 存取子區塊中的程式碼是依序執行。這有時可以惡作劇,比方你可以寫成第一個方法不會被執行或是讓委派方法的執行順序反過來之類的,狠一點還可以用亂數決定順序,乖孩子不要學。

 

      C#

       在 C# 怎麼做?因為 C# 沒有 RaiseEvent 這檔事,所以寫法看起來會有點不一樣:

       (1) 第一種寫法一樣是用一個自定義的 List<T> 來儲存委派。


	public class EventSample
	{
		public delegate void TestValueHandler(EventSample sender, Boolean value);
		private List<TestValueHandler> delegateList = new List<TestValueHandler>();
		public event TestValueHandler TestValueChanged
		{
			add
			{
				lock (delegateList)
				{ delegateList.Add(value); }
			}
			remove
			{
				lock (delegateList)
				{ delegateList.Remove(value); }
			}
		}

		public Boolean TestValue
		{
			set { OnTestValueChanged(value); }
		}

		public String Name { get; set; }
		
		protected virtual void OnTestValueChanged(Boolean value)
		{
			foreach (TestValueHandler handler in delegateList)
			{
				handler(this, value);
			}
		}
	}

       由於 C# 中的事件只有 add 和 remove 兩個存取子,而沒有 RaiseEvent 存取子,因此它執行委派方法的部份會寫在 OnTestValueChanged 方法之中;還有一點不同於 Visual Basic 的是在 C# 中不需要使用 Custom 關鍵字來宣告。

 

       (2)  第二種寫法一樣用 [ MulticastDelegate.GetInvocationList 方法 ]


		public delegate void TestValueHandler(EventSample sender, Boolean value);
		private TestValueHandler TestValueChangedDelegate;
		public event TestValueHandler TestValueChanged
		{
			add
			{
				if (TestValueChangedDelegate == null)
				{ TestValueChangedDelegate += value; }
				else
				{
					lock (TestValueChangedDelegate)
					{ TestValueChangedDelegate += value; }
				}
			}
			remove
			{
				if (TestValueChangedDelegate != null)
				{
					lock (TestValueChangedDelegate)
					{ TestValueChangedDelegate -= value; }
				}
			}
		}

		protected virtual void OnTestValueChanged(Boolean value)
		{
			if (TestValueChangedDelegate != null)
			{
				foreach (TestValueHandler handler in TestValueChangedDelegate.GetInvocationList())
				{
					handler(this, value);
				}
			}
		}

 

       至於 C# 的 Form 程式則列示如下:


	public partial class Form1 : Form
	{
		public Form1()
		{
			InitializeComponent();
		}

		private EventSample sample;
	
		private void Form1_Load(object sender, EventArgs e)
		{
			sample = new EventSample();
			sample.Name = "範例物件";
			sample.TestValueChanged += new EventSample.TestValueHandler(sample_TestValueChanged00);
			sample.TestValueChanged += new EventSample.TestValueHandler(sample_TestValueChanged01);
		}

		private void button1_Click(object sender, EventArgs e)
		{
			sample.TestValue = true;
		}

		private void button2_Click(object sender, EventArgs e)
		{
			sample.TestValue = false;
		}

		private void sample_TestValueChanged00(EventSample  sender, Boolean value)
		{
			MessageBox.Show(String.Format("引發事件的物件名稱:{0}", sender.Name));
		}

		private void sample_TestValueChanged01(EventSample sender, Boolean value)
		{
			MessageBox.Show(String.Format("測試值為:{0}", value));
		}
	}

 

       關於委派和事件的故事先在這邊暫停,在這一篇各位就可以知道其實事件也有很多可以自行定義的地方,包含在新增或移除委派進事件的時候也是可以做些不同的事,自定義的事件也有其它的用途,比方像 MSDN 文件庫舉的兩個例子  [ HOW TO:宣告自訂事件以避免封鎖 (Visual Basic) ] 與  [ HOW TO:宣告自訂事件以節省記憶體 (Visual Basic) ],在下一篇來講簡單的跨執行緒委派後,再回頭來講當委派、事件與跨執行緒他們一起出現的故事。

 

       註:在寫這篇文寫到一半的時候,Clark 也發了一篇  [ [.NET] Thread Separate Event ] ,這篇文就是牽扯到委派、事件與跨執行緒,很值得大家研究一下。