標籤:style blog color io 使用 java ar for 資料
.NET中把“事件”看作一個基本的編程概念,並提供了非常優美的文法支援,對比如下C#和Java代碼可以看出兩種語言設計思想之間的差異。
// C#
someButton.Click += OnSomeButtonClick;
// Java
someButton.addActionListener( new ActionListener(){ public void actionPerformed(){ ... } });
在我們的軟體中就大量使用事件來對監聽者與發行者解耦,但也遇到了一些局限,在這裡跟大家分享一二。一是無法保證監聽者的調用順序;二是當監聽者很多時的監聽、解除監聽的效率問題。
事件監聽者的調用順序
.NET的事件監聽機制對監聽者的調用順序沒有明確的保證,但有時我們卻要求保證不同組件之間的處理順序。比如,在我們的軟體中使用類似解譯器模式的方式來實現使用者互動操作,一個稱作互動源的組件負責將UI控制項上的事件指派給一組稱為互動器的組件,這些組件依照事先確定的優先順序依次獲得事件處理的機會,只有當具有高優先順序的互動器沒有處理事件時,低優先順序的組件才能執行進一步的處理。這樣,我們就能在不同業務功能的實現中通過以不同的順序組織互動器來重用它們。比如,重用一些基本的視圖縮放、平移、菜單處理等功能。
在上述情境下,如何保證互動器間事件處理的順序就變得很重要了。當然如果你看一下MulticastDelegate的原始碼的話,可以知道在當前的實現中其實各個監聽者還是有一定的調用順序的。但一來這屬於實現細節,在將來完全可能改變;二來如果不同的監聽器位於不同的模組中時,要依賴於這一實現而保證它們之間的調用順序也是很困難的。
在這裡我們借鑒了Java中以介面進行事件處理的方式,並在添加監聽器的同時接收一個表示優先順序的參數,這樣就可以明確的維護各個監聽器的順序了,如下面的代碼所示。我們在互動器(IInteractor)介面中為每一個UI事件定義了相應的方法,並且讓InteractSource負責將控制項上的事件轉化為對介面中相應方法的調用。
public class InteractSource{ public void AddInteractor(int priority, IInteractor interactor) { }}public interface IInteractor{ public void OnMouseDown(MouseEventArgs e) { } ... ...}監聽器添加與移除的效率
MulticastDelegate是我們平常使用的事件(event)機制背後的實現,通過其原始碼可以看到,它在內部使用數組儲存了對各個監聽器的引用。這就會造成一個問題——當對一個事件的監聽器數目很多時,添加和移除監聽器的效率將會變得非常低。以移除為例,對於有N個監聽器的事件來說,平均要進行N/2次比較才能確定監聽器的位置,而且還要有額外的數組整理操作。為瞭解決這一情況,我們先是嘗試自行定義事件的添加、移除邏輯,並在內部嘗試使用字典、雜湊表等多種方式進行儲存,但事實證明,雖然二者在時間複雜度上有優勢,不過其實際效率還是達不到要求。
最好狀態下是要有一種能在常數時間內添加和移除監聽器的資料結構,也許你也想到了——雙向鏈表。
也許你又想到了——在雙向鏈表中添加和刪除是常數時間,但尋找卻仍然是O(n)的複雜度。
使用介面形式的設計方式再次展現了其靈活性,我們可以將事件發行者的設計為如下形式(示意代碼):
public class EventSource{ private LinkedList list = new LinkedList(); public Tocken AddListener(IEventListener listener) { LinkedListNode n = new LinkedListNode(listener); list.AddLast(n); return new Tocken(node); } public void RemoveListener(Tocken tocken) { list.Remoe(tocken.node); } public class Tocken { internal LinkedListNode node; }}
在此類中使用雙向鏈表格儲存體已經添加的監聽器,而在AddListener方法每次調用時都將所添加的鏈表節點儲存到一個令牌(Token)中返回。監聽者需要儲存這個令牌,並使用它來解除監聽。當然,監聽者完全可以忽略令牌是個什麼東西,就像地鐵票從來就是只是一張票而已,我們不曾關心它包含著什麼資訊。不過對於發行者來說卻可以將一些定位資訊儲存在其中,從而在解除監聽時充分利用,在上面的代碼中我就儲存了鏈表節點的引用,從而達到監聽者的添加、定位、移除都在常數時間內完成。
當然,還可以在Tocken中儲存發行者的引用,這樣就可以發現”取消對一個從來沒有監聽過的對象的監聽“這樣的BUG。或者,還有其它資訊。
.NET事件監聽機制的局限與擴充