.NET事件监听机制的局限与扩展
.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。或者,还有其它信息。