事件概述 委托是一種類型可以被實例化,而事件可以看作將多播委托進行封裝的一個對象成員(簡化委托調用列表增加和刪除方法)但並非特殊的委托,保護訂閱互不影響。 基礎事件(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_xxx和remove_xxxC#編譯器還自動為方法生成代碼調用(System.Delegate的靜態方法Combine和Remove)。
4.試圖刪除從未添加過的方法,Delegate的Remove方法內部不做任何事經,不會拋出異常或任何警告,事件的方法集體保持不變。
5.add和remove方法以線程安全的一種模式更新值(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
...