Try   HackMD

[C#]使用委派(Delegate)與事件(Event)

tags: C# Programming
Log
  • 2019/05/01 version 1
  • 2022/01/19 更新錯誤範例與補充用法

委派(Delegate)

  • 委派是一種將方法(method)安全封裝的類別,描述需封裝函數的參數型別回傳型別
  • 可以使用++=--=,將方法加入至委派的清單中
  • 引動(Invoke)委派時,委派會依序呼叫清單內的方法,並將傳入委派的參數傳入各方法中
  • 欲將委派物件的清單清空時,直接使委派指派為null即可
  • 22/01/19補充: 可以使用=賦值運算子,使用的話跟賦值一樣會把前一個值蓋過去。

基本使用

/* * 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的方法清單內方法印出來

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: 該功能至少需要有C# 6

複製基本使用最後的一段code(Line: 54 ~ 55)

... ar3 = null; // 清空整個方法清單 ar3(18, 0); // ERROR ==> 方法清單已經被清空了,拋出例外 ...

這個地方如果直接這樣使用會丟出例外,所以呼叫前會先進行null check

ar3 = null; // 清空整個方法清單 if (ar3 == null) // SAFE! { ar3(18, 0); // ERROR ==> 方法清單已經被清空了,拋出例外 }

C#6之後可以有更簡潔的寫法

ar3 = null; // 清空整個方法清單 ar3?.Invoke(18, 0); // SAFE! 呼叫invoke前,已經先檢查是否為null

關於這個寫法?.稱作Null 條件運算子

事件(Event)

  • C#的事件(event)可以讓類別或物件在發生相關事件時,通知其他類別或物件。
  • 在定義事件(Event)之前,必須先定義委派(delegate)的型別,表示事件發生時,呼叫其他類別或物件的函數
  • 只能使用+=-=來註冊或取消註冊事件。

注意: 當註冊該事件的類別或物件毀滅時,請務必許消註冊事件。

例子:

  • A物件發生某件事情時,同時改變B物件C物件的狀態,例如: 在按下SPACE按鍵時B物件C物件會同時回應A物件,直到回應五次。

Main

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

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

class ObjectB { public void ObjBGreeting(ObjectA objectA) { Console.WriteLine("Hello, " + objectA.Name + ". This is Object B."); } }

Object 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不等的參數值,最後一個參數為回傳值的型別。

    例如在上面介紹委派的例子可以這樣改寫:

    ​​​​// 使用內建的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

    ​​​​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

    ​​​​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

    ​​​​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

    ​​​​class ObjectB ​​​​{ ​​​​ public void ObjBGreeting(object sender, ObjectAEventArgs args) ​​​​ { ​​​​ Console.WriteLine("Hello, " + args.Name + ". This is Object B."); ​​​​ } ​​​​}

    Object C

    ​​​​class ObjectC ​​​​{ ​​​​ public void ObjCGreeting(object sender, ObjectAEventArgs args) ​​​​ { ​​​​ Console.WriteLine("I love you, " + args.Name + ". This is Object C."); ​​​​ } ​​​​}

    ObjectAEventArgs

    ​​​​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)

參考資料