一、事件的本質
事件是軟體系統裡的兩個子系統之間,或者兩個模組之間,或者兩個對象之間發送訊息,並處理訊息的過程。在物件導向的世界裡,就可以統一認為是兩個對象之間的行為。
兩個對象之間發送的這種訊息,對發送方來講是產生一個事件,對接受方來講是需要處理某個事件。這種訊息可以是使用者操作產生的或者軟體系統裡的某個對象產生的。
對象之間的事件處理
從可見,對象一產生一個事件,這個事件發生以後需要對象二執行某種動作。這就是事件機制。對象一是事件的產生者,或者寄件者;對象二是事件的接收者或者訂閱者。對象一產生某種訊息,需要對象二響應並處理這給訊息,這就是事件的本質。
以往的很多軟體系統都在採用事件機制處理很多問題。例如從最本質的電腦體系中的非強制中斷處理,到masm中的jump,到c/c++中的回呼函數等等。只不過越進階的軟體系統處理事件或者其提供的很多處理方法越接近人的思維,而越遠離機器思維。構建軟體系統的方法從本質上就是從機器思維走向人的思維的過程。
二、事件機制的好處
1、直接調用
採用事件機制有什麼好處?事件寄件者為什麼不直接呼叫事件接受者提供的處理函數呢?
調用機制
如果所示,兩個對象之間的調用機制。對象B調用對象A的方法,可以通過函數指標或者跳轉(組合語言)等實現。這種方法造成的結果是A和B的緊密耦合,即B對A有很強的依賴性。可以看成B是事件的發行者,A是事件的響應和處理者。不過這種機制用事件機制解釋從理論上就比較牽強了。同一種事物,其實現的思想不一樣。
現在假設有個對象C也要響應B的事件。那麼,按照上面的這種機制,需求修改對象B的代碼,調用對象C的方法。這樣機製造成了非常強的依賴關係。代碼的修改和擴充非常麻煩。如果對象越多,這種關係越多,整個系統越複雜。如果一個系統裡面對象很多,這種依賴關係也很多的情況下,這種調用關係就會十分複雜,對系統的健壯性和優良性會造成影響。
2、回調機制
如果按照c#的委託思想,B需要事先提供對事件處理函數的某些回調指標。這樣,其它對象,例如A和C就去修改它的回調指標,把自己的方法聯絡到上面。但是它們之間的耦合關係就比上面簡單了。
回調機制
回調機制的思想已經比較接近委託的概念。其實委託在本質上也就和回調指標差不多,只是概念上更加進階。對象B作為事件的發行者,事先定義一些回呼函數指標,然後在本地合適的地方調用這些指標指向的函數。而事件訂閱者或者處理者A和C所作的就是讓給這些null 指標賦值,把自己的事件處理方法賦給它,從而實現B調用A和C的方法。
在 C 或 C++ 中與委託最為相似的是函數指標。然而,函數指標只能引用靜態函數,而委託可以引用靜態方法和執行個體方法。當委託引用執行個體方法時,委託不僅儲存對方法進入點的引用,還儲存對為其調用該方法的類執行個體的引用。與函數指標不同,委託是物件導向、型別安全並且安全的。
三、事件機制的實現
1、委託的局限
如果單純用委託,對於事件的發行者B來說,假設它發布事件e,對於事件e,它目前已經知道有A和C對象需要訂閱這個事件。所以,它就申明兩個委派物件引用(本質上類似於函數指標),然後讓A和C對象來採用類似回調的機制訂閱和響應事件。
如果後來,有個對象D也需要訂閱B的事件e,它怎麼辦呢?一種情況是D修改B的一個委派物件引用,把自己的處理方法封裝成一個委派物件付給它。這樣,D就搶奪了A或者C的訂閱。否則,就需要修改B的代碼,添加一個類似的委派物件引用,以便讓D來使用。
這樣做的後果是事件發行者B需要申明很多委派物件的引用變數。結果是弄得代碼維護比較混亂,並且使用者也很多,依賴關係也不容易搞清楚,容易發生錯誤。
2、事件的引入
有了委託,就提供了類似回調一樣的功能。但是,回調機制需要事件發行者和事件訂閱者雙方的共同參與和努力。也就是,每增加一個訂閱者,那麼發行者對象就需要提供一個委託引用,讓訂閱者掛鈎。
如果事件的發行者發布一個事件以後就不在關心誰來訂閱它,那麼以後的處理就交給了使用者,而發行者不再關心事件處理者的問題。
訂閱機制
C#事件的事件就是這種訂閱機制,真正的訂閱。發行者不需要關心訂閱者。
C#事件給訂閱者提供了對事件響應的註冊和反註冊功能。訂閱和撤銷完全是事件接受方的行為。
C#事件機制的實現包括以下幾步:
1、 事件發行者定義一個委託類型;
2、 事件發行者定義一個事件,並且關聯到已經定義的委託上。
3、 事件訂閱者需要產生一個委託執行個體,並把它添加到委託列表。
所以,事件event可以看成是一個事件列表,訂閱者可以註冊和撤銷自己的響應和處理機制,但是它沒有辦法更改整個列表(原則上)。所以,提供了更強、更安全的方式。
四、事件機制的代碼執行個體
應用程式結構圖
,事件發布對象發布一個事件;事件訂閱對象訂閱和處理該事件。
using System;namespace EventExample{ ///<summary> /// MainClass : 主應用程式類 ///</summary> class MainClass { ///<summary> ///應用程式的主進入點。 ///</summary> [STAThread] static void Main(string[] args) { EventPublisher publisher = new EventPublisher(); EventReader1 reader1 = new EventReader1(publisher); EventReader2 reader2 = new EventReader2(publisher); publisher.DoSomthing(); Console.WriteLine("This program already finished!"); Console.ReadLine(); } } ///<summary> /// EventPublisher : 事件的發行者。 ///</summary> public class EventPublisher { // 第一步是申明委託 public delegate int sampleEventDelegate(string messageInfo); // 第二步是申明與上述委託相關的事件 public event sampleEventDelegate sampleEvent; public EventPublisher() { } public void DoSomthing() { /* ... */ // 激發事件 if(this.sampleEvent != null) { this.sampleEvent("hello world!"); } /* ... */ } } ///<summary> /// EventReader1 : 事件的訂閱者1。 ///</summary> public class EventReader1 { public EventReader1(EventPublisher publisher) { publisher.sampleEvent += new EventExample.EventPublisher.sampleEventDelegate(ResponseEvent); } private int ResponseEvent(string msg) { Console.WriteLine(msg + " --- This is from reader1"); return 0; } } ///<summary> /// EventReader2 : 事件的訂閱者2。 ///</summary> public class EventReader2 { public EventReader2(EventPublisher publisher) { publisher.sampleEvent += new EventExample.EventPublisher.sampleEventDelegate(ResponseEvent);publisher.sampleEvent += new EventExample.EventPublisher.sampleEventDelegate(ResponseEvent); } private int ResponseEvent(string msg) { Console.WriteLine(msg + " --- This is from reader2"); Console.WriteLine("Please:down enter key!"); Console.ReadLine(); Console.WriteLine("ok"); return 0; } }}
程式運行結果
總結:事件發行者發布的事件在實質上可以看成對外提供的回呼函數指標列表。這個列表的容量可以動態增長。事件訂閱者可以把自己的事件註冊到這個列表或者撤銷註冊,但是它從原則上無法更改或者對其它訂閱者的註冊產生影響。事件發行者通過兩種手段使得訂閱者正確地使用事件機制:一是定義一種delegate委託類型,事件訂閱者只能按照這種類型定義事件的處理方法;二是定義與這個委託相關的event對象,使得訂閱者只負責註冊和撤銷自己的處理過程而不能隨意對別人的處理過程產生影響。
從運行結果和reader2對象把同一個處理方法註冊了兩次的前提可以看到,對於一個事件,同一個訂閱者可以把同一個處理過程註冊多次,而這個方法最終也會被執行多次。
執行事件訂閱列表中方法的順序不能被保證;而且,在這裡採用的是同步調用方法,只有一個響應函數執行完畢,其它函數才會被執行。如果要方法不被阻塞(包括這裡的等待使用者輸入等),就需要採用非同步呼叫方式。
程式運行結果
總結:事件發行者發布的事件在實質上可以看成對外提供的回呼函數指標列表。這個列表的容量可以動態增長。事件訂閱者可以把自己的事件註冊到這個列表或者撤銷註冊,但是它從原則上無法更改或者對其它訂閱者的註冊產生影響。事件發行者通過兩種手段使得訂閱者正確地使用事件機制:一是定義一種delegate委託類型,事件訂閱者只能按照這種類型定義事件的處理方法;二是定義與這個委託相關的event對象,使得訂閱者只負責註冊和撤銷自己的處理過程而不能隨意對別人的處理過程產生影響。
從運行結果和reader2對象把同一個處理方法註冊了兩次的前提可以看到,對於一個事件,同一個訂閱者可以把同一個處理過程註冊多次,而這個方法最終也會被執行多次。
執行事件訂閱列表中方法的順序不能被保證;而且,在這裡採用的是同步調用方法,只有一個響應函數執行完畢,其它函數才會被執行。如果要方法不被阻塞(包括這裡的等待使用者輸入等),就需要採用非同步呼叫方式。