.NET事件监听机制的局限与扩展分析

本文实例分析了.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。或者,还有其它信息。

希望本文所述对大家的C#程序设计有所帮助。

(0)

相关推荐

  • .NET开发基础:从简单的例子理解泛型 分享

    从简单的例子理解泛型话说有家影视公司选拔偶像派男主角,导演说了,男演员,身高是王道.于是有下面代码:  复制代码 代码如下: //男演员实体类public class Boy{    //姓名    private string mName;    //身高    private int mHeight;    public string Name {        get { return this.mName; }    }    public int Height {        get

  • .net泛型通用函数的特殊问题的解决方法

    自从2.0版本的net framework推出之后泛型(Generic)得到了广泛好评.它不必像object类型一样性能上因为"拆箱"或者"装箱"得到损失,同时在编译语法检测阶段就可以实时检测出传入或者传出的类型是否符合特定条件. 但"金无赤足,人无完人"--在我们享受这些幸福编程的同时,泛型自身类型的不确定也带来了一个显著的问题--无法进行运算符重载.譬如现在我要写一个函数(一个通用的选择排序算法,使用泛型T),该怎么办呢?如果你简单使用这样的

  • .net使用自定义类属性实例

    一般来说,在.net中可以使用Type.GetCustomAttributes获取类上的自定义属性,可以使用PropertyInfo.GetCustomAttributes获取属性信息上的自定义属性.   下面以定义一个简单数据库表的映射实体类来说明相关的使用方法,基于自定义类属性和自定义类中的属性的自定义属性,可以方便的进行类标记和类中属性的标记   创建一个类的自定义属性,用于标识数据库中的表名称,需要继承自Attribute类: 复制代码 代码如下: [AttributeUsage(Att

  • ASP.Net巧用窗体母版页实例

    本文实例讲述了ASP.Net巧用窗体母版页的方法.分享给大家供大家参考.具体分析如下: 背景:每个网页的基本框架结构类似: 浏览网站的时候会发现,好多网站中,每个网页的基本框架都是一样的,比如,最上面都是网站的标题,中间是内容,最下面是网站的版权.开发提供商等信息: 在这些网页中,表头.底部的样式和内容都是一样的,不同的只是中间的内容. 因此在制作网站时,可以将这些共同的东西分离出来,放到"窗体母版页"中,在需要的时候嵌套就可以. 巧用窗体母版项: 下面就开始行动(本文是以Visual

  • 使用.NET中的Action及Func泛型委托深入剖析

    委托,在C#编程中占有极其重要的地位,委托可以将函数封装到委托对象中,并且多个委托可以合并为一个委托,委托对象则可以像普通对象一样被存储.传递,之后在任何时刻进行调用,因此,C#中函数回调机制的实现基本上依赖于委托.C#的delegate关键字用于声明委托,它具有将声明委托类型映射到System.Delegate类的能力,System.Delegate类位于mscorlib.dll中,是.NET的基础核心类之一.使用delegate关键字声明一个委托,实质上创建了System.Delegate的

  • .NET基础之自定义泛型分析

    本文实例分析了.NET基础之自定义泛型.分享给大家供大家参考.具体分析如下: 在.NET中泛型使用非常频繁,在控制台应用程序中,默认的引入了System.Collection.Generics名称空间,其中就提供了我们经常使用的泛型:List<T>和Dictionary<T>,相信用过它们的都知道它们的强大.还有一种我们经常使用的简单的泛型:System.Nullable<T>,即可空类型.我们可以:   System.Nullable<int> nulla

  • .net自定义事件示例分享

    1.新建一个控制台应用程序TestDelegate,本项目主要实现:热水器加热,报警器监控,当热水温度达到80度的时候报警器报警这样一个简单的事件处理程序 2.定义委托处理程序 复制代码 代码如下: public delegate void PlayGameHandler(object sender, System.EventArgs e); 3.添加一个报警器类,报警方法只有在温度超过80度的时候会被调用 复制代码 代码如下: // 负责报警的人    public class 报警器   

  • asp.net自定义分页控件示例

    一..ascx页面 复制代码 代码如下: <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Pagination.ascx.cs" Inherits="IOCS.WEB.UserControl.Pagination" %><link href="../Content/Css/Pager.css" rel="s

  • 关于asp.net 自定义分页控件

    这几天空学习了下自定义控件,参考了aspnetpager开发了自己的分页控件.相对aspnetpager来说功能是多,但个人感觉他的代码太多. 界面: 使用: <%@ Register assembly="YSM.AspNetPager" namespace="YSM.AspNetPager" tagprefix="cc1" %> 页面注册控件,也可以在web.config中配置 1.ajax之UpdatePanel分页则把控件放到U

  • asp.net自定义控件中注册Javascript问题解决方案

    复制代码 代码如下: protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); RenderJS(); } private void RenderJS() { if (!Page.ClientScript.IsClientScriptBlockRegistered(SCRIPT_ID))//如果还没有注册语句,则注册 { Page.ClientScript.RegisterClientScriptBlock(t

随机推荐