###### tags: `Code Sense` `Design-Pattern` # C# Delgate&Event、Action、Fun解說與元件事件互動簡易解說 近期工作上遇到Desktop客製元件,元件間須自行撰寫事件驅動,在過程中會大量使用委派。委派使用為CSharp較進階的語法,對初學者不太好懂,網路上大致的解說與使用情境較沒這麼容易理解,故整理一篇解釋CSharp中的delgate、event、action與function的設計用意並簡易的描述元件互動情境。 ## 我們來訂閱妹子IG 先不談正事,大部分人一定都用過IG對吧? 當我們訂閱某個性感妹子的IG時,一定會希望妹子IG能自動通知傳到你的頁面。那此時我們該如何實作這個功能?答案就是實作觀察者模式! 觀察者模式最重要會有兩個物件,一個是被觀察的observable物件,另一個則是observer物件。如果要以觀察者模式實作,想當然observable被觀察者就是我們的正妹,而觀察者則就是我們的訂閱者。 在我們實作正妹的observer物件之前,先宣告通知的介面方法OnStoryChange,如下 ``` csharp= public interface IFollowersBroadcast { void OnStoryChange(string message); } ``` 接著實作被觀察的正妹,被觀察者通常會需要紀錄有哪些觀察者外,接著就是實作兩個方法,一個是訂閱方法(Add)一個取消訂閱方法(Remove)。 我們開始實作正妹IG,被觀察者(正妹)需要記錄哪些男生訂閱了他,所以會有一個IFollowersBroadcast的List資料去記錄有哪些觀察者,並去實作訂閱與取消訂閱這兩件事情。因為我們使用IG做例子,這裡我故意寫成追蹤(Follower)與取消追蹤(UnFllower)。接著就是最重要的通知實作,但正妹動態改變時,則會通知所有的追中者,這裡很簡單的就用一個For去通知每一個追蹤者。 ``` csharp= public class SexyGirlInstagramWithObservablePattern { /// <summary> /// event /// </summary> private List<IFollowersBroadcast> Broadcasts { get; set; } public SexyGirlInstagramWithObservablePattern() { Broadcasts = new List<IFollowersBroadcast>(); } public void Followers(IFollowersBroadcast broadcast) { Broadcasts.Add(broadcast); } public void UnFllowers(IFollowersBroadcast broadcast) { Broadcasts.Remove(broadcast); } public void BroadCast(string storMessage) { foreach (var item in Broadcasts) { item.OnStoryChange("Observable" + storMessage); } } } ``` 被觀察者實作完後,接著我們去實作觀察者,這裡我用追隨者為物件名稱,程式碼如下,觀察者最重要唯一一件事情就是要去實作被通知事件 IFollowersBroadcast ``` csharp= public class Follower : IFollowersBroadcast { public string Name { get; private set;} public Follower(string name) { Name = name; } public void OnStoryChange(string message) { Console.WriteLine($"{Name} get message=>{message}"); } } ``` 接著我們要在Context實作使用者訂閱妹子這件事情,程式碼如下 ``` csharp= static void Main(string[] args) { var followerMario = new Follower("Mario"); var followerTom = new Follower("Tom"); var sexyGirlObservable = new SexyGirlInstagramWithObservablePattern(); // Mario追蹤了妹子 sexyGirlObservable.Followers(followerMario); // Tom追蹤了妹子 sexyGirlObservable.Followers(followerTom); // 妹子發布動態 sexyGirlObservable.BroadCast("I am so beautiful"); // Tom取消追蹤 sexyGirlObservable.UnFllowers(followerTom); // 妹子又發了一次動態 sexyGirlObservable.BroadCast("I am so beautiful!!!!"); Console.ReadLine(); } ``` 結果如下,可看到在Mario與Tom追蹤正妹後,正妹一發布我很正,Mario與Tom就會收到動態消息。而在第二次發布我很正時,因為Tom取消追蹤,所以只有Mario能收到訊息。 ![](https://i.imgur.com/yv6zMbH.png) ## 訂閱改成Delgate 除了觀察者模式外,Delgate有可以達到同樣的事情。 Delgate語意上稱呼為委派,有託付任務的涵義。上述IG的例子,我們也可以用委派實作。將"通知"這件任務由委派去處理。在C#中,有delgate關字的委派宣告,宣告委派需要設置相對事件涵式的Format如下,有點像是宣告一個方法記憶體的容器,當使用Invoke時,則就會到註冊的方法記憶體位置去執行。 ``` csharp= public delegate void BroadcastOnStoryChangeHandler(string message); ``` 基本上delgate有點像是宣告一個方法記憶體的容器,雖然我們可以單純的使用delgate的Add與Remove,但我們還是做一個簡單的封裝,避免使用者直接操作delgate,他就很像觀察者模式,只是實際註冊的是方法的記憶體位置 ``` csharp= public class SexyGirlInstagramWithDelgate { /// <summary> /// 模擬List<IBroadCast> /// </summary> private List<BroadcastOnStoryChangeHandler> OnBroadcastOnStoryChangeHandler = new List<BroadcastOnStoryChangeHandler>(); public void Followers(BroadcastOnStoryChangeHandler broadcastOnStoryChangeHandler) { OnBroadcastOnStoryChangeHandler.Add(broadcastOnStoryChangeHandler); } public void UnFllowers(BroadcastOnStoryChangeHandler broadcastOnStoryChangeHandler) { OnBroadcastOnStoryChangeHandler.Remove(broadcastOnStoryChangeHandler); } public void BroadCast(string storyMessage) { foreach (var item in OnBroadcastOnStoryChangeHandler) { item.Invoke("Delgate:" + storyMessage); } } } ``` 純用delgate大體上會跟觀察者模式很像,委派其實就是起源於觀察者設計模式!但用委派就只需專注在"事件"註冊即可,他相依事件本身,而不是相依整個物件。使用Context如下 ``` csharp= static void Main(string[] args) { var followerMario = new Follower("Mario"); var followerTom = new Follower("Tom"); var bigFollower = new BigFollower("Nathan"); var sexyGirlDelgate = new SexyGirlInstagramWithDelgate(); // Mario追蹤了妹子 sexyGirlDelgate.Followers(followerMario.OnStoryChange); // Tom追蹤了妹子 sexyGirlDelgate.Followers(followerTom.OnStoryChange); // Nathan追蹤了妹子 sexyGirlDelgate.Followers(bigFollower.OnStoryChange); sexyGirlDelgate.BroadCast("I am so beautiful"); // Mario被老婆發現趕快退訂閱 sexyGirlDelgate.UnFllowers(followerMario.OnStoryChange); sexyGirlDelgate.BroadCast("I am so beautiful!!!!"); Console.ReadLine(); } ``` 結果如下,但如果只有這樣,其實跟觀察者模式也沒有相差太多....所以C#有了event的設計。 ![](https://i.imgur.com/TsalCT3.png) ## Delgate與Event Delgate通常我們會搭配Event使用,Event其實就是一個觀察者模式的封裝 上述例子,我們針對正妹發布動態宣告一個event型態如下(BroadcastOnStoryChangeHandler為delgate) ``` csharp= // 相當於 List<BroadcastOnStoryChangeHandler> OnBroadcastOnStoryChangeHandler // 相當於 Followers(BroadcastOnStoryChangeHandler broadcastOnStoryChangeHandler) // 相當於 UnFllowers(BroadcastOnStoryChangeHandler broadcastOnStoryChangeHandler) public event BroadcastOnStoryChangeHandler OnBroadcastOnStoryChangeHandler; ``` 使用event時,我們就不需要額外實作Followers與UnFllowers。event本身就是觀察者模式的封裝。我們只需要用+=與-=,就可以去註冊事件。 另外一個很重要的點在於event可以封裝delgate,簡單來說就是避免delgate被直接使用,如果能被直接使用,代表他人就可以輕易取消掉你的註冊!完整code如下 ``` csharp= public class SexyGirlInstagramWithEvent { // 相當於 List<BroadcastOnStoryChangeHandler> OnBroadcastOnStoryChangeHandler // 相當於 Followers(BroadcastOnStoryChangeHandler broadcastOnStoryChangeHandler) // 相當於 UnFllowers(BroadcastOnStoryChangeHandler broadcastOnStoryChangeHandler) public event BroadcastOnStoryChangeHandler OnBroadcastOnStoryChangeHandler; public void BroadCast(string message) { OnBroadcastOnStoryChangeHandler?.Invoke("Event:"+message); } } ``` 竟然沒有實作Followers與UnFllowers,那我們該如何註冊?我們使用+=或者-=當作實際註冊與解註冊,實際Cotext Code如下 ``` csharp= static void Main(string[] args) { var followerMario = new Follower("Mario"); var followerTom = new Follower("Tom"); +var sexyGirlEvent = new SexyGirlInstagramWithEvent(); // Mario追蹤了妹子 sexyGirlEvent.OnBroadcastOnStoryChangeHandler += followerMario.OnStoryChange; // Tom追蹤了妹子 sexyGirlEvent.OnBroadcastOnStoryChangeHandler += followerTom.OnStoryChange; sexyGirlEvent.BroadCast("I am so beautiful"); // Tom取消追蹤了妹子 sexyGirlEvent.OnBroadcastOnStoryChangeHandler -= followerTom.OnStoryChange; sexyGirlEvent.BroadCast("I am so beautiful"); Console.ReadLine(); } ``` 實際結果如下,基本上會了event,就能做很多物件間通知的彈性設計,但C#提供一個更簡易的方法就是Action與Func,泛型重載委派,整合了event與delgate的用法。 ![](https://i.imgur.com/5q9czl8.png) ## Action Action稱為重載委派,整合event與delgate,差異在使用上程式設計師能快速實作事件通知,不需要則額外宣告delgate跟event。若在正妹IG例子,我們使用Action需宣告一個BroadcastOnStoryChangeHandler事件,且須帶入發布的message string,則設定宣告方法如下 ``` csharp= public Action<string> BroadcastOnStoryChangeHandler; ``` ! 只有這樣? 沒錯,其實他其實就有點等同於 delegate BroadcastOnStoryChangeHandler(string arg)然後搭event。個人覺得他除了就是讓你少打一些字快速使用外其餘沒太大差別(帶入參數在Action<type1,type2,type3...>設置)。完成Code如下,因為我們需要帶入發布內容,有一個string的帶入參數型態,所以則需宣告Action<string>,相對的如果我們要跟著帶入例如時間之類的就宣告成Action<string, DataTime>。 ```csharp= public class SexyGirlInstagramWithAction { public Action<string> BroadcastOnStoryChangeHandler; public void BroadCast(string message) { BroadcastOnStoryChangeHandler?.Invoke("Event:" + message); } } ``` 使用Context如下,有沒有感覺跟event整個超像!只是實作上些微不同,Action感覺會用得更直覺。 ```csharp= static void Main(string[] args) { var followerMario = new Follower("Mario"); var followerTom = new Follower("Tom"); var sexyGirlAction = new SexyGirlInstagramWithAction(); // Mario追蹤了妹子 sexyGirlAction.BroadcastOnStoryChangeHandler += followerMario.OnStoryChange; // Tom追蹤了妹子 sexyGirlAction.BroadcastOnStoryChangeHandler += followerTom.OnStoryChange; sexyGirlAction.BroadCast("I am so beautiful"); // Tom取消追蹤了妹子 sexyGirlAction.BroadcastOnStoryChangeHandler -= followerTom.OnStoryChange; sexyGirlAction.BroadCast("I am so beautiful"); Console.ReadLine(); } ``` 另外有另一個可回傳值的封裝叫Func,使用上大同小異,差異在設置時,如果有兩個參數以上,最後一個參數則就是回傳型態類別。例如Func<int,string>,代表帶入int參數,回傳型態類別則為string。 ## 元件互動 基本上在客製元件時,元件間的互動則就會很常使用到事件(訂閱者模式),例如下述為3D Switch專案中的MEMS Carrier View,在點選Chip狀態時,GridView的背景色也需要跟著連動。 ![](https://i.imgur.com/Gf7FzLI.png) 我們使用Action模擬實作Element互動,首先我們先建置CarrierViewElement物件,並用Action宣告ChangeChipStatus事件,我們需要帶入Chip ID(int)與Status(string),因此Action Type設置上要有一個int與string(<int,string>)如果用delgate則就是public delgate ChangeChipStatus(int chipID, string status))。 接著宣告SetChipNoStatus方法實作,觸發時使用Invoke通知Actio事件 ```csharp= public class CarrierViewElement { public Action<int, string> ChangeChipStatus; public void SetChipNoStatus(int chipNo, string status) => ChangeChipStatus.Invoke(chipNo, status); } ``` 接著建置GridView物件,宣告一個OnChipStatusChange方法(此方法會註冊到CarrierViewElement的ChangeChipStatus事件中) ```csharp= public class GridViewElement { public void OnChipStatusChange(int chipNo, string status) => Console.WriteLine($"收到{chipNo}Chip改變{status}狀態,做變色處理"); } ``` 實作Context如下,我們模擬簡易應用 ```csharp= static void Main(string[] args) { var carrierView = new CarrierViewElement(); var gridviewElement = new GridViewElement(); // 註冊事件 carrierView.ChangeChipStatus += gridviewElement.OnChipStatusChange; // 設置ChipNo Status carrierView.SetChipNoStatus(1,"Cut Off"); } ``` 當carrierView呼叫SetChipNoStatus(WindowsForm則是滑鼠事件去觸發),當下則就會去通知GridViewElement,此時GridViewElement收到通知後就可以做相對應的動作與處理。 當你學會了事件驅動(委派)後,在需求時做上的設計彈性與完成客製功能的實作能力就能大大增加!