惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Privacy International News Feed
T
The Exploit Database - CXSecurity.com
Cisco Talos Blog
Cisco Talos Blog
Schneier on Security
Schneier on Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy & Cybersecurity Law Blog
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
Scott Helme
Scott Helme
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Latest news
Latest news
P
Proofpoint News Feed
P
Palo Alto Networks Blog
K
Kaspersky official blog
I
Intezer
The Last Watchdog
The Last Watchdog
Google DeepMind News
Google DeepMind News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
小众软件
小众软件
G
GRAHAM CLULEY
Last Week in AI
Last Week in AI
量子位
IT之家
IT之家
F
Full Disclosure
T
Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
W
WeLiveSecurity
Y
Y Combinator Blog
V
V2EX
L
LINUX DO - 热门话题
Martin Fowler
Martin Fowler
月光博客
月光博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
Docker
Google DeepMind News
Google DeepMind News
博客园 - Franky
M
MIT News - Artificial intelligence
爱范儿
爱范儿
I
InfoQ
C
Cisco Blogs
L
LangChain Blog
J
Java Code Geeks
N
News | PayPal Newsroom
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Vercel News
Vercel News
Hugging Face - Blog
Hugging Face - Blog
S
Secure Thoughts
A
About on SuperTechFans

博客园 - 徐鸿翼

《Programming .Net Components》学习笔记(十七) 离开家乡,前往北京 LINQ to SQL可视化调试工具 LINQ to SQL更新数据库操作 《Programming .Net Components》学习笔记(十六) 《Programming .Net Components》学习笔记(十四) LINQ的数据库查询应用 《Programming .Net Components》学习笔记(十三) 代码的坏味道,重构,模式 《Programming .Net Components》学习笔记(十二) 《辐射3》的宣传动画 《Programming .Net Components》学习笔记(十一) Visual Studio “Orcas”中LINQ设计器的使用 《Programming .Net Components》学习笔记(十) 乔布斯与盖茨历史性对话实录 LINQ的数据库支持 《Programming .Net Components》学习笔记(九) 记住今天,《Starcraft II》在暴雪精英赛上公布 《Programming .Net Components》学习笔记(八)
《Programming .Net Components》学习笔记(十五)
徐鸿翼 · 2007-07-14 · via 博客园 - 徐鸿翼

事件访问器

为了挂钩订阅者到发布者,可以直接访问发布者的事件成员变量。但以公共的方法暴露类成员会带来一些麻烦。它违反了封装性和信息隐藏的核心面向对象设计原则,并且使所有订阅者与具体的成员变量定义相耦合。为了减轻这个问题,C#提供了一个类似于属性的机制,称为“event accessor(事件访问器)”。访问器提供类似于属性的优点,在保持原有易用性的同时隐藏实际的类成员。C#使用add和remove——分别执行+=和-=操作符函数,从而封装了事件成员变量:

    using MyEventHandler = GenericEventHandler<string>;

    public class MyPublisher

    {

        MyEventHandler m_MyEvent;

        public event MyEventHandler MyEvent

        {

            add

            {

                m_MyEvent += value;

            }

            remove

            {

                m_MyEvent -= value;

            }

        }

        public void FireEvent()

        {

            EventsHelper.Fire(m_MyEvent, "Hello");

        }

    }

    public class MySubscriber

    {

        public void OnEvent(string message)

        {

            MessageBox.Show(message);

        }

}

    //客户端代码

    MyPublisher publisher = new MyPublisher();

    MySubscriber subscriber = new MySubscriber();

    publisher.MyEvent += subscriber.OnEvent;

    publisher.FireEvent();

publisher.MyEvent -= subscriber.OnEvent;

管理大数量事件

想像一下,当开发框架时会常常遇到发布大量事件的类。例如,在System.Windows.Forms命名空间下的Control类有数十个事件对应多个窗口消息。处理如此数目众多的事件带来的问题是为每个事件分配一个类成员是不切实际的:类定义,文档,CASE工具图解,甚至智能感知都会变得难以管理。为解决这个窘境,.NET提供了EventHandlerList类(System.ComponentModel命名空间下):

    public sealed class EventHandlerList : IDisposable

    {

        public EventHandlerList();

        public Delegate this[object key] { get; set;}

        public void AddHandler(object key, Delegate value);

        public void AddHandlers(EventHandlerList listToAddFrom);

        public void RemoveHandler(object key, Delegate value);

        public virtual void Dispose();

    }

EventHandler是一个存储键/值对的线性列表。键是一个惟一标识事件的对象,值则是System.Delegate的一个实例。由于索引是一个object,所以它可以是一个整数索引,或是一个字符串,一个具体的Button实例等等。分别使用AddHandler和RemoveHandler来新增和移除单独的事件处理方法。也可以使用AddHandlers方法添加一个已存在的EventHandlerList的内容。为了激发事件,就需要使用具有键对象的索引来访问事件列表,得到一个System.Delegate对象。赋值到实际事件委托并激发对象。

下面的例子展示了当要实现一个类似于Windows Froms的按钮类的MyButton类时,如何使用EventHandlerList。该按钮支持很多事件,如鼠标点击和鼠标移动,都导向到同一事件列表。使用事件访问器做到对于客户端的完全封装:

    using ClickEventHandler=GenericEventHandler<MyButton, EventArgs>;

    using MouseEventHandler=GenericEventHandler<MyButton, MouseEventArgs>;

    public class MyButton

    {

        EventHandlerList m_EventList;

        public MyButton()

        {

            m_EventList = new EventHandlerList();

            /* 其余初始化工作 */

        }

        public event ClickEventHandler Click

        {

            add

            {

                m_EventList.AddHandler("Click", value);

            }

            remove

            {

                m_EventList.RemoveHandler("Click", value);

            }

        }

        public event MouseEventHandler MouseMove

        {

            add

            {

                m_EventList.AddHandler("MouseMove", value);

            }

            remove

            {

                m_EventList.RemoveHandler("MouseMove", value);

            }

        }

        void FireClick()

        {

            ClickEventHandler handler = m_EventList["Click"] as ClickEventHandler;

            EventsHelper.Fire(handler, this, EventArgs.Empty);

        }

        void FireMouseMove(MouseButtons button, int clicks, int x, int y, int delta)

        {

            MouseEventHandler handler = m_EventList["MouseMove"] as MouseEventHandler;

            MouseEventArgs args = new MouseEventArgs(button, clicks, x, y, delta);

            EventsHelper.Fire(handler, this, args);

        }

        /* 其他方法和事件定义 */

    }

上面代码存在的问题是例如鼠标移动或鼠标点击事件过于频繁地被提出,并且创建一个新字符串作为键,这样的每次调用对托管堆增加了压力。一个更好的方法是使用实现安排好的静态键集,在所有实例中共享它们:

    public class MyButton

    {

        EventHandlerList m_EventList;

        static object m_MouseMoveEventKey = new object();

        public event MouseEventHandler MouseMove

        {

            add

            {

                m_EventList.AddHandler(m_MouseMoveEventKey, value);

            }

            remove

            {

                m_EventList.RemoveHandler(m_MouseMoveEventKey, value);

            }

        }

        void FireMouseMove(MouseButtons button, int clicks, int x, int y, int delta)

        {

            MouseEventHandler handler = m_EventList[m_MouseMoveEventKey] as MouseEventHandler;

            MouseEventArgs args = new MouseEventArgs(button, clicks, x, y, delta);

            EventsHelper.Fire(handler, this, args);

        }

        /* 其他方法和事件定义 */

    }

编写接收接口

通过隐藏实际的事件成员,事件访问器仅仅满足了封装性的要求。对于该模型还可以继续改进。举例说明,考虑一个订阅者希望订阅一组事件的情况。在这种情况下,为什么要使用多种潜在的高消耗的调用来建立和解除发布者和订阅者之间的连接? 为什么订阅者需要事先知道事件访问器的信息?如果订阅者想要接收接口上的事件,而不是单独的方法应怎样?下面提供一种简单但通用的方式来管理发布者和订阅者之间的连接——可以保存冗余的调用,封装事件访问器和成员,并允许接收接口。现在考虑定义了一组事件的接口,IMySubcriber接口:

    public interface IMySubscriber

    {

        void OnEvent1(object sender, EventArgs eventArgs);

        void OnEvent2(object sender, EventArgs eventArgs);

        void OnEvent3(object sender, EventArgs eventArgs);

    }

谁都可以实现该接口,并且该接口应当被发布者完全获知:

    public class MySubscriber : IMySubscriber

    {

        #region IMySubscriber 成员

        public void OnEvent1(object sender, EventArgs eventArgs)

        {...}

        public void OnEvent2(object sender, EventArgs eventArgs)

        {...}

        public void OnEvent3(object sender, EventArgs eventArgs)

        {...}

        #endregion

    }

下面定义一个事件的枚举类型,并使用Flags属性标记该枚举类型。Flags属性表明该枚举类型的值可以进行位屏蔽(注意EventType.OnAllEvents的定义)。这就允许使用|(OR)位操作符组合不同的枚举值或者使用&(AND)操作符屏蔽它们:

    [Flags]

    public enum EventType

    {

        OnEvent1,

        OnEvent2,

        OnEvent3,

        OnAllEvent=OnEvent1|OnEvent2|OnEvent3

    }

发布者提供了两个方法,Subscribe()和Unsubscribe(),每个都接收两个参数:接口和一个位屏蔽标记来指明要订阅到接收接口的事件。在内部,发布者可以对于接收接口上的每个方法拥有一个事件委托成员,或者对于所有方法使用一个(这属于实现细节,对于订阅者隐藏)。下面示例代码中对于接收接口中的每个方法使用了一个事件成员变量。也展示了具有错误处理功能的Subscribe(),UnSubscribe()和FireEvent()方法:

    public class MyPublisher

    {

        MyEventHandler m_Event1;

        MyEventHandler m_Event2;

        MyEventHandler m_Event3;

        public void Subscribe(IMySubscriber subscriber, EventType eventType)

        {

            if ((eventType & EventType.OnEvent1) == EventType.OnEvent1)

            {

                m_Event1 += subscriber.OnEvent1;

            }

            if ((eventType & EventType.OnEvent2) == EventType.OnEvent2)

            {

                m_Event2 += subscriber.OnEvent2;

            }

            if ((eventType & EventType.OnEvent3) == EventType.OnEvent3)

            {

                m_Event3 += subscriber.OnEvent3;

            }

        }

        public void Unsubscribe(IMySubscriber subscriber, EventType eventType)

        {

            if ((eventType & EventType.OnEvent1) == EventType.OnEvent1)

            {

                m_Event1 -= subscriber.OnEvent1;

            }

            if ((eventType & EventType.OnEvent2) == EventType.OnEvent2)

            {

                m_Event2 -= subscriber.OnEvent2;

            }

            if ((eventType & EventType.OnEvent3) == EventType.OnEvent3)

            {

                m_Event3 -= subscriber.OnEvent3;

            }

        }

        public void FireEvent(EventType eventType)

        {

            if ((eventType & EventType.OnEvent1) == EventType.OnEvent1)

            {

                EventsHelper.Fire<object, EventArgs>(m_Event1, this, EventArgs.Empty);

            }

            if ((eventType & EventType.OnEvent2) == EventType.OnEvent2)

            {

                EventsHelper.Fire<object, EventArgs>(m_Event2, this, EventArgs.Empty);

            }

            if ((eventType & EventType.OnEvent3) == EventType.OnEvent3)

            {

                EventsHelper.Fire<object, EventArgs>(m_Event3, this, EventArgs.Empty);

            }

        }

    }

订阅和撤销订阅的代码要求是一样的,这样就可以优雅地使用一个调用来接受整个接口,同时完全封装了实际的事件类成员:

    MyPublisher publisher = new MyPublisher();

    IMySubscriber subscriber = new MySubscriber();

    //订阅到事件12

    publisher.Subscribe(subscriber, EventType.OnEvent1 | EventType.OnEvent2);

    //只激发事件1

    publisher.FireEvent(EventType.OnEvent1);

根据原版英文翻译,所以不足和错误之处请大家不吝指正,谢谢:)