# [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)