# 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`是一個委派定義的名稱。 ![image](https://hackmd.io/_uploads/Bk1iLJaR1x.png) 既然`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`的定義一模一樣。 ![image](https://hackmd.io/_uploads/BJ9NQQRAye.png) - 接著前往這個Form的`Designer.cs`,會找到 ![image](https://hackmd.io/_uploads/ryckO7C0yx.png) - 再進一步找`Click`的定義,會在`Control.cs`的中繼層看到 ![image](https://hackmd.io/_uploads/BkrtdX0Rye.png) 原來`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)