再谈C#中的委托和事件
寫在最前
我相信全網(wǎng)關(guān)于委托和事件的文章和概述,大家應(yīng)該已經(jīng)讀過很多篇。但是就我的觀察來看,大多數(shù)文在講述這方面概念時,都會用燒開水和狗叫主人的例子來講述事件怎么工作,這樣比喻固然與生活聯(lián)系緊密,但看多了難免有一些審美疲勞。
所以今天,我打算結(jié)合自己的一些工作經(jīng)歷,再來談?wù)勎覀€人對委托和事件的理解,希望能帶給大家一些不一樣的見解。
先談概念
委托:一種引用類型,表示對具有特定參數(shù)列表和返回類型的方法的引用。在實例化委托時,你可以將其實例與任何具有兼容簽名和返回類型的方法相關(guān)聯(lián)。你可以通過委托實例調(diào)用方法。委托用于將方法作為參數(shù)傳遞給其他方法。事件處理程序就是通過委托調(diào)用的方法。
事件:類或?qū)ο罂梢?strong>通過事件向其他類或?qū)ο笸ㄖl(fā)生的相關(guān)事情。發(fā)送(或引發(fā))事件的類稱為“發(fā)布者”,接收(或處理)事件的類稱為“訂閱者”。
以上概述來自MSDN官方文檔:
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/delegates/
從概念中我們其實已經(jīng)可以看出,委托主要是對方法的一種引用,而事件則充當(dāng)了多個類或者對象進行相互通知的橋梁。
如果我這么解釋你可以明白的話,那么我們今天的主題就已經(jīng)明朗了,下面我們就用具體的代碼實例來講述。
再看例子
委托
我們需要先聲明一個委托實例,在C#中,顯示聲明自定義委托采用delegate關(guān)鍵字,聲明方式與聲明普通方法相同,需要指定訪問范圍和返回類型,同時包含訪問參數(shù)。
同時我們針對委托,聲明對應(yīng)的方法,方法的返回值和參數(shù)需要與委托保持一致,若不一致則會在委托傳遞方法時出現(xiàn)編譯錯誤。
委托執(zhí)行內(nèi)部傳遞方法的方式是使用Invoke方法,此處需注意,C#中同時提供了BeginInvoke和EndInvoke的方法對,用于異步執(zhí)行內(nèi)部的方法,具體含義和用法可參考我之前的一篇文章:
淺談.Net異步編程的前世今生----APM篇
下面我們一起來看一下示例:
using System;namespace DelegateAndEvent {class Program{public delegate void DelegateWithNoParams();public delegate int DelegateSum(int a, int b);static void Main(string[] args){DelegateWithNoParams delegate1 = new DelegateWithNoParams(FunctionWithNoParams);delegate1.Invoke();DelegateSum delegate2 = new DelegateSum(FunctionSum);int c = delegate2.Invoke(10, 20);Console.WriteLine("帶返回值和參數(shù)的方法,結(jié)果為:" + c);Console.Read();}public static void FunctionWithNoParams(){Console.WriteLine("無返回值無參數(shù)的方法");}public static int FunctionSum(int a, int b){int c = a + b;return c;}} }在此示例中,我們分別定義了一個無參數(shù)無返回值的委托和一個包含2個參數(shù)并返回int類型的委托,分別用于執(zhí)行兩種對應(yīng)的方法。在兩個委托執(zhí)行對應(yīng)的Invoke方法之后,會產(chǎn)生以下的結(jié)果:
結(jié)果和我們預(yù)期一致,程序同步順序地執(zhí)行了兩個委托并打印出相應(yīng)的結(jié)果。但是看到這里也許你會有一個疑問,既然委托執(zhí)行時的結(jié)果與直接調(diào)用方法一致,那么我們?yōu)槭裁催€需要使用委托來執(zhí)行方法呢?
這時我們就要回到最初的定義:委托用于將方法作為參數(shù)傳遞給其他方法。
由于實例化的委托是一個對象,因此可以作為參數(shù)傳遞或分配給一個屬性。這允許方法接受委托作為參數(shù)并在稍后調(diào)用委托。這被稱為異步回調(diào),是在長進程完成時通知調(diào)用方的常用方法。當(dāng)以這種方式使用委托時,使用委托的代碼不需要知道要使用的實現(xiàn)方法。功能類似于封裝接口提供的功能。
我們一起使用一個比較直觀的例子來驗證:
using System;namespace ConsoleApp1 {class Program{public delegate void Del(string message);static void Main(string[] args){Del handler = new Del(DelegateMethod);MethodWithCallback(1,2,handler);Console.Read();}public static void DelegateMethod(string message){Console.WriteLine(message);}public static void MethodWithCallback(int param1, int param2, Del callback){callback(string.Format("當(dāng)前的值為:{0}", (param1 + param2)));}} }在這段代碼中,我們聲明了一個無返回值委托Del,用于接收傳入的消息,并且該委托指向了一個調(diào)用控制臺的方法DelegateMethod。而后續(xù)我們調(diào)用MethodWithCallback方法時,無需調(diào)用控制臺相關(guān)方法,而是直接將Del委托的實例作為參數(shù)傳入,就實現(xiàn)DelegateMethod方法的調(diào)用。這個實例就是我們上述提到的異步回調(diào)和委托對方法的引用。
運行結(jié)果如下:
此處我使用了JetBrains出品的IDE Rider,因此截圖界面會與VS有所不同,喜歡輕量級IDE的同學(xué)可以試試這款,有30天的免費試用期,地址:Rider: The Cross-Platform .NET IDE from JetBrains,此處不再過多講解。
根據(jù)我們上述的實例講解,大家對委托的作用及使用場景應(yīng)該有了初步的理解,但是我們仔細(xì)想一想,上述的場景似乎少了些什么,為什么我們的委托始終只能指向一個方法進行調(diào)用呢?
這里就要引出我們接下來的概念:多播委托。
事實上,委托是可以調(diào)用多個方法的,這種方式就叫做多播委托,在C#中,我們可以使用+=的運算符,將其他委托附加到當(dāng)前委托之后,就可以實現(xiàn)多播委托,相關(guān)示例如下:
using System;namespace ConsoleApp1 {class Program{public delegate void Del();static void Main(string[] args){Del handler = new Del(DelegateMethod1);Del handlerNew = new Del(DelegateMethod2);handler += handlerNew;handler.Invoke();Console.Read();}public static void DelegateMethod1(){Console.WriteLine("天才第一步!");}public static void DelegateMethod2(){Console.WriteLine("天才第二步!");}} }在這個示例中,我們重新編寫了一個方法叫DelegateMethod2,同時我們又聲明了一個新的委托對象handlerNew指向該方法。接著我們使用+=的方式將handlerNew添加至handler并執(zhí)行該委托,得到的結(jié)果如下:
如我們先前所料,多播委托把多個方法依次進行了執(zhí)行。此時如果某個方法發(fā)生異常,則不會調(diào)用列表中的后續(xù)方法,如果委托具有返回值和/或輸出參數(shù),它將返回上次調(diào)用方法的返回值和參數(shù)。與增加方法相對應(yīng),若要刪除調(diào)用列表的方法,則可以使用-=運算符進行操作。
關(guān)于委托的理解與常用方式,我們就講解到這里,事實上,多播委托常用于事件處理中,由此可見,事件與委托有著千絲萬縷的聯(lián)系,下面我們就拉開事件的序幕。
事件
如前文講解時所說,事件是一種通知行為,因此要分為事件發(fā)布者和事件訂閱者。而且在.Net中,事件基于EventHandler委托和EventArgs基類的,因此我們在聲明事件時,需要先定義一個委托類型,然后使用event關(guān)鍵字進行事件的定義。
相關(guān)的示例如下:
using System;namespace ConsoleApp1 {public class PublishEvent{public delegate void NoticeHandler(string message);public event NoticeHandler NoticeEvent;public void Works(){//觸發(fā)事件OnNoticed();}protected virtual void OnNoticed(){if (NoticeEvent != null){//傳遞事件及參數(shù)NoticeEvent("Notice發(fā)布的報警信息!");}}}public class SubscribEvent{public SubscribEvent(PublishEvent pub){//訂閱事件pub.NoticeEvent += PrintResult;}/// <summary>/// 訂閱事件后的響應(yīng)函數(shù)/// </summary>/// <param name="message"></param>void PrintResult(string message){Console.WriteLine(string.Format("已收到{0}!采取措施!",message));}}class Program{static void Main(string[] args){PublishEvent publish = new PublishEvent();SubscribEvent subscrib = new SubscribEvent(publish);//觸發(fā)事件publish.Works();Console.Read();}} }從事例中我們可以看出,我們分別定義了發(fā)布者和訂閱者相關(guān)的類。
在發(fā)布者中,我們需要聲明一個委托NoticeHandler,然后定義一個此類型的事件NoticeEvent。在定義對象之后,我們需要對事件進行執(zhí)行,因此有了OnNoticed方法,此方法只用于事件本身的執(zhí)行。那么什么時候才能執(zhí)行事件呢?于是我們又有了觸發(fā)該事件的方法Works,當(dāng)Works方法被調(diào)用時,就會觸發(fā)NoticeEvent事件。
而在訂閱者中,我們需要對NoticeEvent事件進行訂閱,因此我們需要發(fā)布者的對象PublishEvent,同時需要對它的事件進行訂閱。正如我們前文所說,訂閱使用+=的方式,與多播委托的使用是一致的,而+=后的對象正是我們需要響應(yīng)后續(xù)處理的方法PrintResult。當(dāng)事件被觸發(fā)時,訂閱者會接收到該事件,并自動執(zhí)行響應(yīng)函數(shù)PrintResult。
執(zhí)行結(jié)果如下圖所示:
從執(zhí)行結(jié)果中我們可以看出,在事件被觸發(fā)后,訂閱者成功接收到了發(fā)布者發(fā)布的事件內(nèi)容,并進行自動響應(yīng),而我們在此過程中從未顯式調(diào)用訂閱者的任何方法,這也是事件模型的本質(zhì)意義:從發(fā)布到訂閱。
在微軟官方文檔中提到,事件是一種特殊的多播委托,只能從聲明它的類中進行調(diào)用??蛻舳舜a通過提供對應(yīng)在引發(fā)事件時調(diào)用的方法的引用來訂閱事件。這些方法通過事件訪問器添加到委托的調(diào)用列表中,這也是我們可以使用+=去訂閱事件的原因所在,而取消事件則和多播委托一致,使用-=的方式。
關(guān)于事件的使用場景還有一種與多線程通知相關(guān)的典型用法,具體含義和用法可參考我之前的另一篇文章:
淺談.Net異步編程的前世今生----EAP篇
最后總結(jié)
本文我們講解了委托和事件之間的關(guān)系,以及它們的使用場景。在我個人的工作經(jīng)歷中,曾經(jīng)有3年左右的時間從事C/S相關(guān)的開發(fā)工作,其中包含了大量多線程、委托和事件的使用場景。主要用于在開發(fā)WinForm程序時,不同窗體(包含父子窗體)之間進行相互通信,其中都是基于事件的發(fā)布和訂閱作為實現(xiàn)的。而委托的使用場景則更多,很多C#方法在使用時都會傳入一個Action或者Func委托作為參數(shù),而這些參數(shù)同時又支持Lambda表達式,這就又引出了匿名函數(shù)的概念,由于篇幅所限,此處不再進行講解,大家可以自行搜索資料進行了解和學(xué)習(xí)。
除了具體的技術(shù)點之外,在我們的設(shè)計模式中也有事件的使用身影,最典型的莫過于觀察者模式。關(guān)于觀察者模式,網(wǎng)上眾說紛紜,也有很多資料會將它與發(fā)布-訂閱模式混為一談。而實際上這兩種模式并不完全是同一種概念和實現(xiàn)方式,那么下一次我們將會從設(shè)計模式著手,談一談觀察者模式和發(fā)布-訂閱模式的異同,敬請期待!
您的點贊和在看是我創(chuàng)作的最大動力,感謝支持
公眾號:wacky的碎碎念
知乎:wacky
總結(jié)
以上是生活随笔為你收集整理的再谈C#中的委托和事件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 简述LINQ的发展历程
- 下一篇: 微服务组件记事本:Skywalking执