# Delegate(委派)、Event(事件)、EventHandler(標準委派事件)
在查詢相關資料時,看到很多種解釋和用法,但總感覺有些痛點沒有解釋清楚,故在此整理。
以下範例,都將使用這三個方法`加(Add)`、`減(Sub)`、`乘(Multiply)`來做示範。
```csharp=
private void Add(int num1, int num2) { //加
Console.WriteLine("Add = " + (num1 + num2).ToString());
}
private void Sub(int num1, int num2) { //減
Console.WriteLine("Sub = " + (num1 - num2).ToString());
}
private void Multiply(int num1, int num2) { //乘
Console.WriteLine("Multiply = " + (num1 * num2).ToString());
}
```
## Delegate(委派)
>委派(delegate)就像「方法(method)的執行清單」,可以將想執行的方法全部加入清單中,引動(invoke)後,將++依序執行++清單內的所有方法。
:::success
##### 💡「將方法加入清單」的動作,稱為「訂閱」,反之為「取消訂閱」。
:::
### 使用方式
- 📌 **定義一個 委派** - *==想加入清單的方法,必須長這樣==*
`[存取修飾詞] delegate 回傳值 委派名稱(傳入值1, 傳入值2, ...);`
- ***傳入值***、***回傳值*** 的型態,必須跟想要訂閱的方法相同。
- `Add`、`Sub`、`Multiply`的 ***傳入值*** 是兩個`int`,***回傳值*** 為`void`。
:::success
##### 💡「回傳值+名稱+傳入值」的這個組合,稱為「簽章(Signature)」。
:::
```csharp=
public delegate void CalculateMath(int num1, int num2);
```
- 📌 **宣告一個 委派** - *==建立清單==*
`[存取修飾詞] 委派名稱 委派變數;`
```csharp=+
CalculateMath myDelegate;
```
- 📌 **訂閱/取消訂閱** - *==加入/移出清單==*
```csharp=+
myDelegate += new CalculateMath(Add); //將方法「累加」到清單尾端
myDelegate -= new CalculateMath(Add); //將方法從清單中「移除」
//將方法「設定」到清單(會覆蓋掉原本的清單)
myDelegate = new CalculateMath(Add);
```
也可以簡化寫法
```csharp
myDelegate += Add; //將方法「累加」到清單尾端
myDelegate -= Add; //將方法從清單中「移除」
//將方法「設定」到清單(會覆蓋掉原本的清單)
myDelegate = Add;
```
- 📌 **引動(invoke)委派** - *==執行清單內的方法==*
```csharp=+
myDelegate.Invoke(2, 5);
```
也可以簡化寫法
```csharp
myDelegate(2, 5);
```
### ✔ **完整範例**
```csharp=
/*
建立一個Form,並加入一個Button。
按下Button,會將方法加入Delegate,然後invoke。
*/
public partial class Form1 : Form
{
public Form1() {
InitializeComponent();
}
public delegate void CalculateMath(int a, int b); //定義Delegate
CalculateMath myDelegate = null; //宣告Delegate
private void Add(int num1, int num2) { //加
Console.WriteLine("Add = " + (num1 + num2).ToString());
}
private void Sub(int num1, int num2) { //減
Console.WriteLine("Sub = " + (num1 - num2).ToString());
}
private void Multiply(int num1, int num2) { //乘
Console.WriteLine("Multiply = " + (num1 * num2).ToString());
}
private void button1_Click(object sender, EventArgs e) {
myDelegate += new CalculateMath(Add); //清單:{Add}
myDelegate += new CalculateMath(Sub); //清單:{Add,Sub}
myDelegate.Invoke(1, 2); //執行清單
myDelegate += new CalculateMath(Multiply); //清單:{Add,Sub,Multiply}
myDelegate -= new CalculateMath(Sub); //清單:{Add,Multiply}
myDelegate.Invoke(3, 4); //執行清單
myDelegate = new CalculateMath(Sub); //清單:{Sub}
myDelegate.Invoke(5, 6); //執行清單
myDelegate = null; //清空
}
}
/**********
執行結果:
Add = 3
Sub = -1
Add = 7
Multiply = 12
Sub = -1
**********/
```
- ✅ **範例小細節**
- `CalculateMath myDelegate = null;` 尚未使用的委派,可先設為`null`。
- `myDelegate.Invoke(1, 2);` 執行`Invoke`,會**依照加入順序**呼叫方法。
- `myDelegate = new CalculateMath(Sub);` 用「`=`」會覆蓋整個清單。
- `myDelegate = null;` 清空委派,直接設為`null`。
## Event(事件)
> Event(事件)就像++加入一些限制的Delegate(委派)++,本質上也是++方法(method)的執行清單++,但宣告時必須指定一個委派,引動(invoke)後會++依序執行++清單內的所有方法。
### 使用方式
- 📌**定義一個 委派** - *==想加入清單的方法,必須長這樣==*
`[存取修飾詞] delegate 回傳值 委派名稱(傳入值1, 傳入值2, ...);`
```csharp=
public delegate void CalculateMath(int num1, int num2);
```
- 📌**宣告一個 事件** - *==建立清單==*
`[存取修飾詞] event 委派名稱 事件變數;`
```csharp=+
event CalculateMath myEvent;
```
- 📌 **訂閱/取消訂閱** - *==加入/移出清單==*
```csharp=+
myEvent += new CalculateMath(Add); //將方法「累加」到清單尾端
myEvent -= new CalculateMath(Add); //將方法從清單中「移除」
//將方法「設定」到清單(會覆蓋掉原本的清單)
myEvent = new CalculateMath(Add);
```
也可以簡化寫法
```csharp
myEvent += Add; //將方法「累加」到清單尾端
myEvent -= Add; //將方法從清單中「移除」
//將方法「設定」到清單(會覆蓋掉原本的清單)
myEvent = Add;
```
- 📌 **引動(invoke)事件** - *==執行清單內的方法==*
```csharp=+
myEvent.Invoke(2, 5);
```
也可以簡化寫法
```csharp
myEvent(2, 5);
```
### ✔ **完整範例**
```csharp=
public partial class Form1 : Form
{
public Form1() {
InitializeComponent();
}
public delegate void CalculateMath(int a, int b); //定義Delegate
event CalculateMath myEvent = null; //宣告Event
private void Add(int num1, int num2) { //加
Console.WriteLine("Add = " + (num1 + num2).ToString());
}
private void Sub(int num1, int num2) { //減
Console.WriteLine("Sub = " + (num1 - num2).ToString());
}
private void Multiply(int num1, int num2) { //乘
Console.WriteLine("Multiply = " + (num1 * num2).ToString());
}
private void button1_Click(object sender, EventArgs e) {
myEvent += new CalculateMath(Add); //清單:{Add}
myEvent += new CalculateMath(Sub); //清單:{Add,Sub}
myEvent.Invoke(1, 2); //執行清單
myEvent += new CalculateMath(Multiply); //清單:{Add,Sub,Multiply}
myEvent -= new CalculateMath(Sub); //清單:{Add,Multiply}
myEvent.Invoke(3, 4); //執行清單
myEvent = new CalculateMath(Sub); //清單:{Sub}
myEvent.Invoke(5, 6); //執行清單
myEvent = null; //清空
}
}
/**********
執行結果:
Add = 3
Sub = -1
Add = 7
Multiply = 12
Sub = -1
**********/
```
## Delegate(委派) 和 Event(事件) 的差異
>使用定義好的委派,宣告一個變數,就會得到一個 ++委派變數++。
>若在宣告前,加一個`event`關鍵字,就會得到一個 ++事件變數++。
:::success
##### 💡 `event`其實就像`delegate`的修飾詞。
:::
```csharp
public delegate void CalculateMath(int num1, int num2); //定義委派
CalculateMath myDelegate; //宣告委派
event CalculateMath myEvent; //宣告事件
```
乍看之下,**委派(delegate)** 和 **事件(event)** 除了宣告有差別,其餘都一樣。
**⚡沒錯!在同個類別(Class)中,兩者用法是一樣的,差異在++不同類別的使用權限++。⚡**
| | 差異 |
| --- |:---|
| 委派 | 在 **A類別** 宣告的 `delegate`,**B類別** ++可以++使用 `=` 覆蓋清單,或 `Invoke` 執行清單。 |
| 事件 | 在 **A類別** 宣告的 `event`,**B類別** ++不可以++使用 `=` 覆蓋清單,或 `Invoke` 執行清單。 |
:::success
##### 📍 無論在哪個類別宣告`delegate`或`event`,所有類別都能用 `+=` 或 `-=` 修改清單。
:::
### ✔ **完整範例**
在 **A類別** 定義`delegate`並宣告`event`,然後在 **B類別** 操作`event`。
**A類別:**
```csharp=
public class ClassA
{
ClassB B = new ClassB(); //建立ClassB物件
public delegate void CalculateMath(int a, int b); //定義Delegate
public event CalculateMath myEvent = null; //宣告Event
public void Add(int num1, int num2) { //加
Console.WriteLine("Add = " + (num1 + num2).ToString());
}
void foo() {
// 自身類別宣告的event,沒有使用限制
myEvent += new CalculateMath(Add);
myEvent += new CalculateMath(B.Sub);
myEvent -= new CalculateMath(Add);
myEvent -= new CalculateMath(B.Sub);
myEvent = new CalculateMath(Add);
myEvent = new CalculateMath(B.Sub);
myEvent = null;
myEvent.Invoke(1, 2);
}
}
```
**B類別:**
```csharp=
public class ClassB
{
ClassA A = new ClassA(); //建立ClassA物件
public void Sub(int num1, int num2) { //減
Console.WriteLine("Sub = " + (num1 - num2).ToString());
}
void foo() {
// 可以用「+=」或「-=」,操作其它類別宣告的event
A.myEvent += new CalculateMath(A.Add);
A.myEvent += new CalculateMath(Sub);
A.myEvent -= new CalculateMath(A.Add);
A.myEvent -= new CalculateMath(Sub);
// 不可用「=」或「Invoke」,操作其它類別宣告的event
A.myEvent = new CalculateMath(A.Add); //error
A.myEvent = new CalculateMath(Sub); //error
A.myEvent = null; //error
A.myEvent.Invoke(1, 2); //error
}
}
```
## EventHandler(標準委派事件)
> EventHandler(標準委派事件),其實是微軟已經事先定義好的委派。
在VS中,對`EventHandler`使用`[移至定義]`,實際到中繼層確認。
如下圖,就能發現`EventHandler`是一個委派定義的名稱。

既然`EventHandler`是一個++委派名稱++,那當然就可以直接用這個委派名稱,宣告委派或事件。
```csharp
public EventHandler MyDelegate; //委派
public event EventHandler MyEvent; //事件
```
:::success
##### 💡 當然也可以不使用預設的`EventHandler`,自己撰寫一模一樣的委派定義,但不必多此一舉。
:::
### EventHandler的定義
```csharp
public delegate void EventHandler(object sender, EventArgs e);
```
`EventHandler`的定義是 ++*沒有回傳值*++、++*兩個傳入值*++ 的委派。
- **傳入值**
- `sender`,`object`型態用來存放物件,通常是觸發該事件的物件。
- `e`,`EventArgs`類別用來存放參數,通常是該事件需要使用的參數。
- **EventArgs**
- `EventArgs`類別,通常會讓其它類別來繼承。
- 這樣就能定義一個專門用來記錄資料的類別,並將此類別的物件,當成參數傳入事件。如果沒有要使用此欄位,建議傳入`EventArgs.Empty`。
```csharp
public class DATA : EventArgs
{
int num;
bool flag;
}
```
### EventHandler的用途
>用逐步回推的方式來理解。
- 如果在VS Form建立一個`Button1`,並且將`Button1`點兩下,就會發現產生的`button1_Click`方法,跟`EventHandler`的定義一模一樣。

- 接著前往這個Form的`Designer.cs`,會找到

- 再進一步找`Click`的定義,會在`Control.cs`的中繼層看到

原來`Click`是在`Control`類別中,用`EventHandler`宣告的++事件變數++。
雙擊`Button1`後,會先建立`button1_Click`方法。
然後在`Designer.cs`用`Click`訂閱`button1_Click`方法。
也就是說,從「產生`Button1`到按下執行`button1_Click`」,其實就是一連串完整的事件操作。
:::success
##### 💡 所以通常說「C#是 ++事件驅動的程式設計++(Event-Driven Programming)」。
:::
## 常見用法
### 📌批次處理
>將要執行的方法,加入清單中,一次全部觸發。
例如,想在按下`Button1`時,同時觸發`Button2`和`Button3`,就可以直接用`Button1`的`Click`事件,訂閱`button2_Click`和`button3_Click`。
而不是在`button1_Click`方法中,呼叫`button2_Click`、`button3_Click`。
### ✔ **完整範例**
```csharp=
public partial class Form1 : Form
{
public Form1() {
InitializeComponent();
button1.Click += new EventHandler(button2_Click);
button1.Click += new EventHandler(button3_Click);
}
private void button1_Click(object sender, EventArgs e) {
Console.WriteLine("This is button1_Click.");
}
private void button2_Click(object sender, EventArgs e) {
Console.WriteLine("This is button2_Click.");
}
private void button3_Click(object sender, EventArgs e) {
Console.WriteLine("This is button3_Click.");
}
}
/**********
按下Button1的結果:
This is button1_Click.
This is button2_Click.
This is button3_Click.
**********/
```
需注意,被訂閱的方法會依序被呼叫,而且 **==不會等待上一個方法執行完畢==**。
若這些方法有先後順序,需要 **==執行結束後,才呼叫清單中的下一個方法==**,必須自行加入等待機制,或是乾脆不要用委派/事件。
### 📌傳遞方法
>將方法當成參數,傳入其它方法。
有時候一大段程式碼,其中一小區塊需要視情況呼叫不同的方法。
例如,現在有一個`Person`類別,當按下`Button1`會產生`student`物件,而按下`Button2`會產生`teacher`物件,並執行對應動作。
📍可能會這樣寫:
```csharp=
public partial class Form1 : Form
{
public Form1() {
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e) {
Person student;
student.StudentSayHello();
}
private void button2_Click(object sender, EventArgs e) {
Person teacher;
teacher.TeacherSayHello();
}
}
public class Person
{
public void StudentSayHello() { //主要邏輯
Console.WriteLine("Hello,");
Console.WriteLine("I am a student,"); //個別流程1
Console.WriteLine("Nice to meet you.");
}
public void TeacherSayHello() { //主要邏輯
Console.WriteLine("Hello,");
Console.WriteLine("I am a teacher."); //個別流程2
Console.WriteLine("Nice to meet you.");
}
}
```
`StudentSayHello`和`TeacherSayHello`方法,大部分內容是相同的,只有其中一段不同。
若只為了這一段不同的程式碼,將一整串完整的流程寫兩次,不是一個好做法。
因為類別中,就會有兩個撰寫主要邏輯,而且內容相似的方法,若後續要修改,就要改兩次。
`ex. 將「Nice to meet you.」改成「Glad to see you.」。`
📍針對上述問題,將相似的內容做整合,並改用判斷式決定要執行哪一段流程:
```csharp=
public partial class Form1 : Form
{
public Form1() {
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e) {
Person student;
student.SayHello(1);
}
private void button2_Click(object sender, EventArgs e) {
Person teacher;
teacher.SayHello(2);
}
}
public class Person
{
public void SayHello(int num) { //主要邏輯
Console.WriteLine("Hello,");
switch (num) {
case 1: //個別流程1
Console.WriteLine("I am a student.");
break;
case 2: //個別流程2
Console.WriteLine("I am a teacher.");
break;
}
Console.WriteLine("Nice to meet you.");
}
}
```
但這樣有個問題,若不同的區塊內容量很多,那判斷式就會變得很長,而且每增加一種職業,還會再變更長,可讀性極差,顯然也不是好解法。
📍若想讓每個物件都能呼叫同一個方法,又能視情況改變執行流程,就可以用委派。
### ✔ **完整範例**
```csharp=
public partial class Form1 : Form
{
public Form1() {
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e) {
Person student;
student.SayHello(Person.StudentSayHello);
}
private void button2_Click(object sender, EventArgs e) {
Person teacher;
teacher.SayHello(Person.TeacherSayHello);
}
}
public class Person
{
public delegate void myDelegate();
public void SayHello(myDelegate myDel) { //主要邏輯
Console.WriteLine("Hello,");
myDel.Invoke();
Console.WriteLine("Nice to meet you.");
}
public static void StudentSayHello() { //個別流程1
Console.WriteLine("I am a student.");
}
public static void TeacherSayHello() { //個別流程2
Console.WriteLine("I am a teacher.");
}
}
```
這種寫法讓類別內,只會有一個撰寫主要邏輯的方法。
不同的區塊,則是用委派的方式,讓方法像參數一樣傳入主要邏輯的方法中。
在維護、擴充程式碼時,就不會動到主要邏輯,而要修改這些視情況執行的區塊時,可讀性更好。
## 參考資料
[[C#]使用委派(Delegate)與事件(Event)](https://hackmd.io/@BKLiang/csharp_delegate_event)
[EventHandler Delegate](https://learn.microsoft.com/en-us/dotnet/api/system.eventhandler?view=net-9.0)
[什麼是EventHandler?](https://ryanchen34057.github.io/2019/10/12/eventHandlerIntro/)
[C# 函数中(object sender, EventArgs e)参数是什么意思](https://blog.csdn.net/qq_41375318/article/details/118325758)
[C# delegate 委派](https://ithelp.ithome.com.tw/articles/10255691)
[[C#][C#幼幼班]簽章Signature](https://jo-jo.medium.com/c-c-%E5%B9%BC%E5%B9%BC%E7%8F%AD-%E7%B0%BD%E7%AB%A0signature-fa9b04e1a3e2)
[委派 (Delegate)](https://vito-note.blogspot.com/2012/03/delegate.html)
[[C#]委派Delegate](https://jo-jo.medium.com/c-%E5%A7%94%E6%B4%BEdelegate-8d71fa299d32)
[.Net委派(delegate)的簡易解說與用法](https://eric0806.blogspot.com/2015/01/dotnet-delegate-usage.html)