事件(event)

来源:https://www.cnblogs.com/jonins/archive/2018/07/18/9324771.html
-Advertisement-
Play Games

事件概述 委托是一種類型可以被實例化,而事件可以看作將多播委托進行封裝的一個對象成員(簡化委托調用列表增加和刪除方法)但並非特殊的委托,保護訂閱互不影響。 基礎事件(event) 在.Net中聲明事件使用關鍵詞event,使用也非常簡單在委托(delegate)前面加上event: 上述代碼執行結果 ...



 

事件概述

委托是一種類型可以被實例化,而事件可以看作將多播委托進行封裝的一個對象成員(簡化委托調用列表增加和刪除方法)但並非特殊的委托,保護訂閱互不影響。

 

基礎事件(event)

在.Net中聲明事件使用關鍵詞event,使用也非常簡單在委托(delegate)前面加上event:

 1     class Program
 2     {
 3         /// <summary>
 4         /// 定義有參無返回值委托
 5         /// </summary>
 6         /// <param name="i"></param>
 7         public delegate void NoReturnWithParameters();
 8         /// <summary>
 9         /// 定義接受NoReturnWithParameters委托類型的事件
10         /// </summary>
11         static event NoReturnWithParameters NoReturnWithParametersEvent;
12         static void Main(string[] args)
13         {
14             //委托方法1
15             {
16                 Action action = new Action(() =>
17                 {
18                     Console.WriteLine("測試委托方法1成功");
19                 });
20                 NoReturnWithParameters noReturnWithParameters = new NoReturnWithParameters(action);
21                 //事件訂閱委托
22                 NoReturnWithParametersEvent += noReturnWithParameters;
23                 //事件取閱委托
24                 NoReturnWithParametersEvent -= noReturnWithParameters;
25             }
26             //委托方法2
27             {
28                 //事件訂閱委托
29                 NoReturnWithParametersEvent += new NoReturnWithParameters(() =>
30                 {
31                     Console.WriteLine("測試委托方法2成功");
32                 });
33             }
34             //委托方法3
35             {
36                 //事件訂閱委托
37                 NoReturnWithParametersEvent += new NoReturnWithParameters(() => Console.WriteLine("測試委托方法3成功"));
38             }
39             //執行事件
40             NoReturnWithParametersEvent();
41             Console.ReadKey();
42         }
43         /*
44          * 作者:Jonins
45          * 出處:http://www.cnblogs.com/jonins/
46          */
47     }

上述代碼執行結果:

 

事件發佈&訂閱

事件基於委托,為委托提供了一種發佈/訂閱機制。當使用事件時一般會出現兩種角色:發行者訂閱者。

發行者(Publisher)也稱為發送者(sender):是包含委托欄位的類,它決定何時調用委托廣播。

訂閱者(Subscriber)也稱為接受者(recevier):是方法目標的接收者,通過在發行者的委托上調用+=和-=,決定何時開始和結束監聽。一個訂閱者不知道也不幹涉其它的訂閱者。

來電->打開手機->接電話,這樣一個需求,模擬訂閱發佈機制:

 1     /// <summary>
 2     /// 發行者
 3     /// </summary>
 4     public class Publisher
 5     {
 6         /// <summary>
 7         /// 委托
 8         /// </summary>
 9         public delegate void Publication();
10 
11         /// <summary>
12         /// 事件  這裡約束委托類型可以為內置委托Action
13         /// </summary>
14         public event Publication AfterPublication;
15         /// <summary>
16         /// 來電事件
17         /// </summary>
18         public void Call()
19         {
20             Console.WriteLine("顯示來電");
21             if (AfterPublication != null)//如果調用列表不為空,觸發事件
22             {
23                 AfterPublication();
24             }
25         }
26     }
27     /// <summary>
28     /// 訂閱者
29     /// </summary>
30     public class Subscriber
31     {
32         /// <summary>
33         /// 訂閱者事件處理方法
34         /// </summary>
35         public void Connect()
36         {
37             Console.WriteLine("通話接通");
38         }
39         /// <summary>
40         /// 訂閱者事件處理方法
41         /// </summary>
42         public void Unlock()
43         {
44             Console.WriteLine("電話解鎖");
45         }
46     }
47     /*
48      * 作者:Jonins
49      * 出處:http://www.cnblogs.com/jonins/
50      */
 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //定義發行者
 6             Publisher publisher = new Publisher();
 7             //定義訂閱者
 8             Subscriber subscriber = new Subscriber();
 9             //發行者訂閱 當來電需要電話解鎖
10             publisher.AfterPublication += new Publisher.Publication(subscriber.Unlock);
11             //發行者訂閱 當來電則接通電話
12             publisher.AfterPublication += new Publisher.Publication(subscriber.Connect);
13             //來電話了
14             publisher.Call();
15             Console.ReadKey();
16         }
17     }

執行結果:

註意

1.事件只可以從聲明它們的類中調用, 派生類無法直接調用基類中聲明的事件。

1  publisher.AfterPublication();//這行代碼在Publisher類外部調用則編譯不通過

2.對於事件在聲明類外部只能+=,-=不能直接調用,而委托在外部不僅可以使用+=,-=等運算符還可以直接調用。

下麵調用方式與上面執行結果一樣,利用了委托多播的特性。

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Publisher publisher = new Publisher();
 6             Subscriber subscriber = new Subscriber();
 7             //------利用多播委托-------
 8             var publication = new Publisher.Publication(subscriber.Unlock);
 9             publication += new Publisher.Publication(subscriber.Connect);
10             publisher.AfterPublication += publication;
11             //---------End-----------
12             publisher.Call();
13             Console.ReadKey();
14         }
15     }

 

 自定義事件(EventArgs&EventHandler&事件監聽器)

有過Windwos Form開發經驗對下麵的代碼會熟悉:

1 private void Form1_Load(object sender, EventArgs e)
2 {
3      ...      
4 }

在設計器Form1.Designer.cs中有事件的附加。這種方式屬於Visual Studio IDE事件訂閱。

1  this.Load += new System.EventHandler(this.Form1_Load);

在 .NET Framework 類庫中,事件基於 EventHandler 委托和 EventArgs 基類。

基於EventHandler模式的事件

 1     /// <summary>
 2     /// 事件監聽器
 3     /// </summary>
 4     public class Consumer
 5     {
 6         private string _name;
 7 
 8         public Consumer(string name)
 9         {
10             _name = name;
11         }
12         public void Monitor(object sender, CustomEventArgs e)
13         {
14             Console.WriteLine($"Name:{_name}; 信息:{e.Message};到底要不要接呢?");
15         }
16     }
17     /// <summary>
18     /// 定義保存自定義事件信息的對象
19     /// </summary>
20     public class CustomEventArgs : EventArgs//作為事件的參數,必須派生自EventArgs基類
21     {
22         public CustomEventArgs(string message)
23         {
24             this.Message = message;
25         }
26         public string Message { get; set; }
27     }
28     /// <summary>
29     /// 發佈者
30     /// </summary>
31     public class Publisher
32     {
33         public event EventHandler<CustomEventArgs> Publication;//定義事件
34         public void Call(string w)
35         {
36             Console.WriteLine("顯示來電." + w);
37             OnRaiseCustomEvent(new CustomEventArgs(w));
38         }
39         //在一個受保護的虛擬方法中包裝事件調用。
40         //允許派生類覆蓋事件調用行為
41         protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
42         {
43             //在空校驗之後和事件引發之前。製作臨時副本,以避免可能發生的事件。
44             EventHandler<CustomEventArgs> publication = Publication;
45             //如果沒有訂閱者,事件將是空的。
46             if (publication != null)
47             {
48                 publication(this, e);
49             }
50         }
51     }
52     /// <summary>
53     /// 訂閱者
54     /// </summary>
55     public class Subscriber
56     {
57         private string Name;
58         public Subscriber(string name, Publisher pub)
59         {
60             Name = name;
61             //使用c# 2.0語法訂閱事件
62             pub.Publication += UnlockEvent;
63             pub.Publication += ConnectEvent;
64         }
65         //定義當事件被提起時該採取什麼行動。
66         void ConnectEvent(object sender, CustomEventArgs e)
67         {
68             Console.WriteLine("通話接通.{0}.{1}", e.Message, Name);
69         }
70         void UnlockEvent(object sender, CustomEventArgs e)
71         {
72             Console.WriteLine("電話解鎖.{0}.{1}", e.Message, Name);
73         }
74     }
75     /*
76      * 作者:Jonins
77      * 出處:http://www.cnblogs.com/jonins/
78      */

調用方式:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Publisher pub = new Publisher();
 6             //加入一個事件監聽
 7             Consumer jack = new Consumer("Jack");
 8             pub.Publication += jack.Monitor;
 9             Subscriber user1 = new Subscriber("中國移動", pub);
10             pub.Call("號碼10086");
11             Console.WriteLine("--------------------------------------------------");
12             Publisher pub2 = new Publisher();
13             Subscriber user2 = new Subscriber("中國聯通", pub2);
14             pub2.Call("號碼10010");
15             Console.ReadKey();
16         }
17     }

結果如下:

1.EventHandler<T>在.NET Framework 2.0中引入,義了一個處理程式,它返回void,接受兩個參數。

1 public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

第一個參數(sender)是一個對象,包含事件的發送者。
第二個參數(e)提供了事件的相關信息,參數隨不同的事件類型而改變(繼承EventArgs)。
.NET1.0為所有不同數據類型的事件定義了幾百個委托,有了泛型委托EventHandler<T>後,不再需要委托了。

2.EventArgs,標識表示包含事件數據的類的基類,並提供用於不包含事件數據的事件的值。

1 [System.Runtime.InteropServices.ComVisible(true)]
2 public class EventArgs

3.同時可以根據編程方式訂閱事件

1     Publisher pub = new Publisher();
2     pub.Publication += Close;
3     ...
4     //添加一個方法
5     static void Close(object sender, CustomEventArgs a)
6     {
7             // 關閉電話
8     }

4.Consumer類為事件監聽器當觸發事件時可獲取當前發佈者對應自定義信息對象,可以根據需要做邏輯編碼,再執行事件所訂閱的相關處理。增加事件訂閱/發佈機制的健壯性。

5.以線程安全的方式觸發事件    

1 EventHandler<CustomEventArgs> publication = Publication;

觸發事件是只包含一行代碼的程式。這是C#6.0的功能。在之前版本,觸發事件之前要做為空判斷。同時在進行null檢測和觸發之間,可能另一個線程把事件設置為null。所以需要一個局部變數。在C#6.0中,所有觸發都可以使用null傳播運算符和一個代碼行取代。

1 Publication?.Invoke(this, e);

註意儘管定義的類中的事件可基於任何有效委托類型,甚至是返回值的委托,但一般還是建議使用 EventHandler 使事件基於 .NET Framework 模式。

 

線程安全方式觸發事件

在上面的例子中,過去常見的觸發事件有三種方式:

 1             //版本1
 2             if (Publication != null)
 3             {
 4                 Publication();//觸發事件
 5             }
 6 
 7             //版本2
 8             var temp = Publication;
 9             if (temp != null)
10             {
11                 temp();//觸發事件
12             }
13 
14             //版本3
15             var temp = Volatile.Read(ref Publication);
16             if (temp != null)
17             {
18                 temp();//觸發事件
19             }

版本1會發生NullReferenceException異常。

版本2的解決思路是,將引用賦值到臨時變數temp中,後者引用賦值發生時的委托鏈。所以temp複製後即使另一個線程更改了AfterPublication對象也沒有關係。委托是不可變得,所以理論上行得通。但是編譯器可能通過完全移除變數temp的方式對上述代碼進行優化所以仍可能拋出NullReferenceException.

版本3Volatile.Read()的調用,強迫Publication在這個調用發生時讀取,引用真的必須賦值到temp中,編譯器優化代碼。然後temp只有再部位null時才被調用。

版本3最完美技術正確,版本2也是可以使用的,因為JIT編譯機制上知道不該優化掉變數temp,所以在局部變數中緩存一個引用,可確保堆應用只被訪問一次。但將來是否改變不好說,所以建議採用版本3。

 

 

事件揭秘

我們重新審視基礎事件里的一段代碼:

1     public delegate void NoReturnWithParameters();
2     static event NoReturnWithParameters NoReturnWithParametersEvent;

通過反編譯我們可以看到:

編譯器相當於做了一次如下封裝:

 1 NoReturnWithParameters parameters;
 2 private event NoReturnWithParameters NoReturnWithParametersEvent
 3 {
 4      add {  NoReturnWithParametersEvent+=parameters; }
 5      remove {  NoReturnWithParametersEvent-=parameters; }
 6 }
 7 /*
 8  * 作者:Jonins
 9  * 出處:http://www.cnblogs.com/jonins/
10  */

聲明瞭一個私有的委托變數,開放兩個方法add和remove作為事件訪問器用於(+=、-=),NoReturnWithParametersEvent被編譯為Private從而實現封裝外部無法觸發事件。

1.委托類型欄位是對委托列表頭部的引用,事件發生時會通知這個列表中的委托。欄位初始化為null,表明無偵聽者等級對該事件的關註。

2.即使原始代碼將事件定義為Public,委托欄位也始終是Private.目的是防止外部的代碼不正確的操作它。

3.方法add_xxxremove_xxxC#編譯器還自動為方法生成代碼調用(System.Delegate的靜態方法CombineRemove)。

4.試圖刪除從未添加過的方法,Delegate的Remove方法內部不做任何事經,不會拋出異常或任何警告,事件的方法集體保持不變。

5.addremove方法以線程安全的一種模式更新值(Interlocked Anything模式)。

 

結語

類或對象可以通過事件向其他類或對象通知發生的相關事情。事件使用的是發佈/訂閱機制,聲明事件的類為發佈類,而對這個事件進行處理的類則為訂閱類。而訂閱類如何知道這個事件發生並處理,這時候需要用到委托。事件的使用離不開委托。但是事件並不是委托的一種(事件是特殊的委托的說法並不正確),委托屬於類型(type)它指的是集合(類,介面,結構,枚舉,委托),事件是定義在類里的一個成員。

 

參考文獻

 

CLR via C#(第4版) Jeffrey Richter

C#高級編程(第7版) Christian Nagel  (版9、10對事件部分沒有多大差異)

果殼中的C# C#5.0權威指南 Joseph Albahari

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/events/index

...


 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、Redis API支持 Python連接redis redis-py安裝方式 Python連接Redis redis連接分片集群 python連接redis sentinel Python String類型使用簡介 Python hash類型使用簡介 Python list類型使用簡介 Pyth ...
  • 如果爬蟲需要展現速度,我覺得就是去下載圖片吧,原本是想選擇去煎蛋那裡下載圖片的,那裡的美女圖片都是高質量的,我稿子都是差不多寫好了的,無奈今天重新看下,妹子圖的入口給關了。 至於為什麼關呢,大家可以去看看XXX日報的關停原因吧或者百度下,這裡就不多說了,這次我選擇了去下載無版權高清圖片,因為做自媒體 ...
  • 當需要向某特定URL地址發送HTTP請求並得到相應響應時,通常會用到HttpClient類。該類包含了眾多有用的方法,可以滿足絕大多數的需求。但是如果對其使用不當時,可能會出現意想不到的事情。 博客園官方團隊就遇上過這樣的 "問題" ,國外博主也記錄過類似的情況, "YOU'RE USING HTT ...
  • 1.第一步環境搭建 運行環境:window 客戶端版本:Go語言geth 下載地址https://ethereum.github.io/go-ethereum/downloads/ 以太坊API中文文檔:http://web3.tryblockchain.org/Web3.js-api-refren ...
  • 書本:https://www.tutorialspoint.com/nhibernate/index.htm 第一天學習內容 概念 Nhibernate是一個ORM框架。 ORM框架:將聲明的類映射到資料庫中。可以不使用SQL語言,減少錯誤。 Demo 1.創建空控制台應用 起名Nhibernate ...
  • 轉載請註入出處: https://home.cnblogs.com/u/zhiyong-ITNote/ dotnet core中提供了一個新的身份驗證框架Identity,它不同於dot net下的身份驗證。在這個框架裡面,有一個生成token的功能,也就是我們常說的令牌,令牌的作用有哪些?Toke ...
  • 開發工具:Visual Studio 2017 C 版本:C 7.1 最有效的防止SQL註入的方式是調用資料庫時使用參數化查詢。 但是如果是接手一個舊的WebApi項目,不想改繁多的資料庫訪問層的代碼,應該如何做。 我的解決方案是加一個過濾器。 先寫過濾方法,上代碼 然後是過濾器,先上代碼 思路是, ...
  • //生成cs文件 public class MD5Help { ///MD5加密 public static string MD5Encrypt(string pToEncrypt, string sKey) { DESCryptoServiceProvider des = new DESCrypt ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...