# [C#]使用委派(Delegate)與事件(Event) ###### tags: `C#` `Programming` :::spoiler Log - 2019/05/01 version 1 - 2022/01/19 更新錯誤範例與補充用法 ::: ## 委派(Delegate) - 委派是一種將方法(method)**安全封裝**的類別,描述需封裝函數的**參數型別**和**回傳型別** - 可以使用`+`、`+=`、`-`、`-=`,將方法加入至委派的清單中 - 引動(Invoke)委派時,委派會依序呼叫清單內的方法,並將傳入委派的參數傳入各方法中 - 欲將委派物件的清單清空時,直接使委派指派為`null`即可 - `22/01/19`補充: 可以使用`=`賦值運算子,使用的話跟賦值一樣會把前一個值蓋過去。 ### 基本使用 ```C#= /* * delegate (回傳型別) [委派類別] (參數型別..) * 宣告一個委派類別 - ArithmeticExpr * 傳入方法(method)的回傳型別為void,參數型別為兩個int */ public delegate void ArithmeticExpr(int a, int b); // 顯示出該方法的名稱與 a + b 的結果 public static void Add(int a, int b) { Console.WriteLine("Method:[Add] has been called.\nThe answer is: " + (a + b).ToString() + "\n"); } // 顯示出該方法的名稱與 a - b 的結果 public static void Sub(int a, int b) { Console.WriteLine("Method:[Sub] has been called.\nThe answer is: " + (a - b).ToString() + "\n"); } // 顯示出該方法的名稱與 a * b 的結果 public static void Multiply(int a, int b) { Console.WriteLine("Method:[Multiply] has been called.\nThe answer is: " + (a * b).ToString() + "\n"); } static void Main(string[] args) { // 宣告三個委派 ArithmeticExpr ar1, ar2, ar3; /*******************Section 1*******************/ ar1 = Add; // 起始將Add指派給ar1 ar1(10, 7); // Invoke委派,呼叫Add並顯示出答案: 17 ar1 = Sub; // ar1指派為Sub ar1(10, 7); // Invoke委派,呼叫Sub並顯示出答案: 3 ar1.Invoke(10, 7); // 或是可以使用Invoke方法 /*******************Section 2*******************/ ar2 = Add; // 起始將Add指派給ar2 ar2(3, 41); // Invoke委派,呼叫Add並顯示出答案: 44 ar2 += Sub; // 將Sub方法加入ar2的方法清單中 // 現在方法清單中有Add和Sub (順序為: Add -> Sub) ar2(3, 41); // Invoke委派,首先呼叫Add,答案為44;而後呼叫Sub,答案為-38 /*******************Section 3*******************/ ar3 = Multiply; // 起始將Multiply指派給ar3 ar3 += Add; // 將Add方法加入ar3的方法清單中 // 現在方法清單中有Multiply和Add (順序為: Multiply -> Add) ar3 += Sub; // 將Sub方法加入ar3的方法清單中 // 現在方法清單中有Multiply、Add和Sub (順序為: Multiply -> Add -> Sub) ar3(67, 19); // Invoke委派,依序呼叫 Multiply、Add和Sub ar3 -= ar2; // 將Add、Sub移出方法清單 ar3(67, 19); // 再次Invoke委派,這次只有ar3被呼叫 ar3 = null; // 清空整個方法清單 ar3(18, 0); // ERROR ==> 方法清單已經被清空了,拋出例外 Console.ReadKey(); } ``` ### 像物件一樣對待它... 這個方法目前不知道可以應用在哪裡,但可以這樣使用。 使用`System.MulticastDelegate.GetInvocationList()`可以獲得委派的方法清單,下列的Code可以將`ar2`的方法清單內方法印出來 ```C#= Delegate[] delArray = ar2.GetInvocationList(); for (int i = 0; i < delArray.Length; i++) { Console.WriteLine("No.{0} - Method: {1}", i + 1, delArray[i].Method.Name); } ``` ### `Null` Check :::warning :warning: 該功能至少需要有C# 6 ::: 複製***基本使用***最後的一段code(Line: 54 ~ 55) ```csharp= ... ar3 = null; // 清空整個方法清單 ar3(18, 0); // ERROR ==> 方法清單已經被清空了,拋出例外 ... ``` 這個地方如果直接這樣使用會丟出例外,所以呼叫前會先進行null check ```csharp= ar3 = null; // 清空整個方法清單 if (ar3 == null) // SAFE! { ar3(18, 0); // ERROR ==> 方法清單已經被清空了,拋出例外 } ``` C#6之後可以有更簡潔的寫法 ```csharp= ar3 = null; // 清空整個方法清單 ar3?.Invoke(18, 0); // SAFE! 呼叫invoke前,已經先檢查是否為null ``` 關於這個寫法`?.`稱作[Null 條件運算子](https://docs.microsoft.com/zh-tw/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-) ## 事件(Event) - C#的事件(event)可以讓類別或物件在**發生相關事件**時,通知其他類別或物件。 - 在定義事件(Event)之前,必須先定義委派(delegate)的型別,表示事件發生時,**呼叫其他類別或物件的函數**。 - **只能**使用`+=`和`-=`來註冊或取消註冊事件。 > **注意: 當註冊該事件的類別或物件毀滅時,請務必許消註冊事件。** **例子:** - 在**A物件**發生某件事情時,同時改變**B物件**和**C物件**的狀態,例如: 在**按下SPACE按鍵時**,**B物件**和**C物件**會同時回應**A物件**,直到回應五次。 > Main ```C#= class Program { static void Main(string[] args) { ObjectA objectA = new ObjectA("Kawaii", 5); ObjectB objectB = new ObjectB(); ObjectC objectC = new ObjectC(); objectA.ObjAnounceEvent += objectB.ObjBGreeting; objectA.ObjAnounceEvent += objectC.ObjCGreeting; while (Console.ReadKey().Key == ConsoleKey.Spacebar) { objectA.OnObjAEventBeenCalled(); } } } ``` > Object A ```C#= class ObjectA { public string Name { get; set; } public int CallCountThreshold { get; set; } private int _callCount; public delegate void ObjADelegate(ObjectA objectA); public event ObjADelegate ObjAnounceEvent; public ObjectA(string name, int callCountThreshold) { Name = name; CallCountThreshold = callCountThreshold; _callCount = 0; } public void OnObjAEventBeenCalled() { _callCount += 1; if (_callCount > CallCountThreshold) { ObjAnounceEvent = null; Console.WriteLine("Clear the invocation list."); Environment.Exit(0); } ObjAnounceEvent?.Invoke(this); } } ``` > Object B ```C#= class ObjectB { public void ObjBGreeting(ObjectA objectA) { Console.WriteLine("Hello, " + objectA.Name + ". This is Object B."); } } ``` > Object C ```C#= class ObjectC { public void ObjCGreeting(ObjectA objectA) { Console.WriteLine("I love you, " + objectA.Name + ". This is Object C."); } } ``` ## .NET **.NET**中有定義**泛型**的委派類別 - **System.Action**: 回傳值為void,可以給予0到16不等的參數值。 - **System.Func**: 如果需要回傳值可以用Func,跟Action一樣,可以給予0到16不等的參數值,最後一個參數為**回傳值**的型別。 例如在上面介紹委派的例子可以這樣改寫: ```C#= // 使用內建的Func進行改寫,最後一個泛型類別為回傳值 public System.Action<int, int> ArithmeticExpr; // 顯示出該方法的名稱與 a + b 的結果 public static void Add(int a, int b) { Console.WriteLine("Method:[Add] has been called.\nThe answer is: " + (a + b).ToString() + "\n"); } // 顯示出該方法的名稱與 a - b 的結果 public static void Sub(int a, int b) { Console.WriteLine("Method:[Sub] has been called.\nThe answer is: " + (a - b).ToString() + "\n"); } // 顯示出該方法的名稱與 a * b 的結果 public static void Multiply(int a, int b) { Console.WriteLine("Method:[Multiply] has been called.\nThe answer is: " + (a * b).ToString() + "\n"); } ``` - **System.EventHandler**: 內部定義兩種EventHandler ```C#= public delegate void EventHandler(object sender, EventArgs e); public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e); ``` **功能**:第一種的EventHandler是不帶*事件資料*的,第二種可以帶我們*自定義*的事件資料。 **首先**來談談參數列(args),兩種EventHandler都有名叫`sender`的參數,顧名思義,該參數指的是發起事件的物件或類別。`e`則是需要傳遞的事件資料。 這樣寫的意義在於統一了事件委派的規則,讓程式碼易讀且好維護。 在傳遞事件的資料時,可以創建一個`class`繼承`EventArgs`,這樣就可把我們想傳遞的訊息往創建的`class`裏頭塞了。 **問題來了**,**如果沒有想傳遞的事件資料呢?** 這時可以使用`System.EventArgs.Empty`,這樣已表示沒有需要傳遞的事件資料了。 接下來,我們依照上面給的情境題進行改寫 : > Main ```C#= class Program { static void Main(string[] args) { ObjectA objectA = new ObjectA("Kawaii", 5); ObjectB objectB = new ObjectB(); ObjectC objectC = new ObjectC(); objectA.ObjAnounceEvent += objectB.ObjBGreeting; objectA.ObjAnounceEvent += objectC.ObjCGreeting; while (Console.ReadKey().Key == ConsoleKey.Spacebar) { objectA.OnObjAEventBeenCalled(); } } } ``` > Object A ```C#= class ObjectA { public string Name { get; set; } public int CallCountThreshold { get; set; } public int CallCount; public EventHandler<ObjectAEventArgs> ObjAnounceEvent; public ObjectA(string name, int callCountThreshold) { Name = name; CallCountThreshold = callCountThreshold; CallCount = 0; } public void OnObjAEventBeenCalled() { CallCount += 1; if (CallCount > CallCountThreshold) { ObjAnounceEvent = null; Console.WriteLine("Clear the invocation list."); Environment.Exit(0); } ObjAnounceEvent?.Invoke(this, new ObjectAEventArgs(Name)); } } ``` > Object B ```C#= class ObjectB { public void ObjBGreeting(object sender, ObjectAEventArgs args) { Console.WriteLine("Hello, " + args.Name + ". This is Object B."); } } ``` > Object C ```C#= class ObjectC { public void ObjCGreeting(object sender, ObjectAEventArgs args) { Console.WriteLine("I love you, " + args.Name + ". This is Object C."); } } ``` > ObjectAEventArgs ```C#= class ObjectAEventArgs : EventArgs { public ObjectAEventArgs(string name) { Name = name; } public string Name { get; set; } } ``` 結語 --- 2019/05/01 - .NET有定義了Action、Func和EventHandler,大多時間不需要定義自己的委派與事件 - 單純使用委派可以做到和事件一樣的效果,兩個(關鍵字)差別在於事件只能使用`+=`、`-=`訂閱與取消訂閱事件,可以確保不能用`=`把事件清單蓋過去 2022/01/19 - 紀錄了一些工作上的用法,C#提供了很方便的方法,實作出觀察者模式(Observer Pattern) ## 參考資料 >* [Unity 事件機制淺談 (C# events, unity events)](https://dev.twsiyuan.com/2017/03/c-sharp-event-in-unity.html) >* [delegate (C# 參考)](https://docs.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/delegate) >* [事件 (C# 程式設計手冊)](https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/events/index) >* [Action<T> Delegate](https://docs.microsoft.com/zh-tw/dotnet/api/system.action-1?redirectedfrom=MSDN&view=netframework-4.7.2) >* [Func<T,TResult> Delegate](https://docs.microsoft.com/zh-tw/dotnet/api/system.func-2?view=netframework-4.7.2) >* [EventHandler Delegate](https://docs.microsoft.com/zh-tw/dotnet/api/system.eventhandler?view=netframework-4.7.2)