• 【C#】详解C#事件


    目录结构:

    contents structure [+]

    在这篇Blog中,笔者会详细阐述C#中事件的使用。

    1.事件基本介绍

    C#中定义了事件成员的类型,允许类型通知其它类型发生了特定的事情。事件是基于委托为基础的,说白了就是对委托的封装,委托就是一种回调方法的机制,笔者认为设计事件就是为了能够更好地理解面向对象。

    事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些出现如系统生成的通知。应用程序需要在事件发生时响应事件。例如,中断。事件是用于进程间通信。

    为了更好地理解事件,这里笔者描述一个场景:有一个按钮,当双击该按钮的时候,很有可能希望其他的动作也被触发。
    如图:

    圆圈1,表示第一步:首先把CallBacker1的callback1()方法和CallBacker2的callbacker2()方法注册到Button的DoubleClick事件中。
    圆圈2,表示第二步:引发Button的DoubleClick。
    圆圈3,表示第三步:触发在注册在Button的DoubleClick事件中的所有回调方法。

    下面笔者将会按照上面的情景来讲解C#中事件的知识点。

    1.1 定义事件类型

    事件引发时,引发事件的对象可能希望向接受事件通知的对象传递一些附加信息。根据约定,这种类应该从System.EventArgs派生,而且类名以EventArgs结束。

    这里笔者定义一个NewButtonClickEventArgs类,用来容纳被点击按钮的文本信息,

        class NewButtonClickEventArgs : EventArgs{
            private readonly String text;
            public NewButtonClickEventArgs(String text) {
                this.text = text;
            }
            public String Text { get { return text; } }
        }

    EventArgs类在Microsoft .NET Framework中定义,EventArgs是一个基类型。
    EventArgs的源码如下:

        [Serializable]
        [System.Runtime.InteropServices.ComVisible(true)]
        public class EventArgs {
            public static readonly EventArgs Empty = new EventArgs();
        
            public EventArgs()
            {
            }
        }

    可以看出EventArgs的类型非常简单,不会附加任何传递信息,主要目的是作为其他类型的基类。当然,如果时间不需要传递任何附加信息,那么就可以用该类。

    1.2 定义事件成员

    在C#中定义事件成员使用event关键字,每个事件成员几乎都会指定以下信息:
    a.可访问性标识符。
    b.委托类型,以及需要委托的原型。
    c.事件名称
    例如:

        sealed class Button {
            //定义事件成员
            public event EventHandler<NewButtonClickEventArgs> DoubleClick;
        ...
        }

    我们指定了EventHanler泛型委托,该委托的元数据如下:
    public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)

    1.3 定义引发事件的方法

    按照约定,类要定义一个受保护的虚方法,但是如果该类是密封的,那么该方法就应该声明为私有的和非虚的。

        sealed class Button {
        ...
            //定义引发事件的方法
            private void OnDoubleClick(NewButtonClickEventArgs e) {
                EventHandler<NewButtonClickEventArgs> temp = Volatile.Read(ref DoubleClick);
                if (temp != null) {
                    temp(this,e);
                }
            }
        ...
        }

    1.3.1 以线程安全的方式引发事件

    在上面定义引发事件的方法中,我们使用了如下的代码:

    EventHandler<NewButtonClickEventArgs> temp = Volatile.Read(ref DoubleClick);

    相信在事件的调用中,经常都会看到如上形式的代码,接下来笔者将会讲解原因:

    在.net Framework刚发布的时建议开发者使用如下的形式引发事件:

    if(DoubleClick!=null){
        DoubleClick(this,e);
    }

    这样做的问题是,虽然当前线程检查出了DoubleClick不为空,但有可能存在如下情况,当前线程在检查了DoubleClick不为空后,在还没调用DoubleClick之前,其它线程修改了DoubleClick,比如移除了委托链上的所有方法,那么当前线程再次调用DoubleClick的时候,就有可能NullReferenceException。
    这是一个竞态问题,我们可以修改如下形式:

    EventHandler<NewButtonClickEventArgs> temp=DoubleClick;
    if(temp!=null){
        temp(this,e);
    }

    它的思路是,把DoubleClick复制到临时变量temp中,这样的话,即使其他线程改变了DoubleClick事件,那么也不会出错。
    但是如果编译器擅自做主,进行优化,移除临时变量temp,那么上面的方式就和第一种方式就没什么区别,仍然有可能抛出NullReferenceException异常。然而,编译器是理解这种这种模式的,不会把temp优化掉优化,所以这是一种安全的方式。
    上面之所以能够安全调用,是因为编译器能够“理解”正确,一般情况下,我们是不太知道编译器是如何理解的,所以能够强制提醒一下编译器就更好了,如下的方法:

               EventHandler<NewButtonClickEventArgs> temp = Volatile.Read(ref DoubleClick);
                if (temp != null)
                {
                    temp(this, e);
                }

    这里使用了Volatile类的Read方法,以线程安全的方式把DoubleClick复制到temp变量中,这样的话,编译器绝不会吧temp变量优化掉。

    1.4. 登记事件关注

    上面我们已经定义好了事件,接下来就是登记事件关注。
    定义如下类:

        class CallBacker{
            public CallBacker(Button btn){
                btn.DoubleClick += CallBack;
            }
            public void CallBack(Object sender,NewButtonClickEventArgs args){
                Console.WriteLine("按钮文本为:"+args.Text);
            }
        }

    在这个类的构造方法中,我们完成对DoubleClick事件的关注。

    到这里我们就完成一个简单的事件过程,完整代码如下:

        class NewButtonClickEventArgs : EventArgs{
            private readonly String text;
    
            public NewButtonClickEventArgs(String text) {
                this.text = text;
            }
    
            public String Text { get { return text; } }
        }
        sealed class Button {
            //定义事件成员
            public event EventHandler<NewButtonClickEventArgs> DoubleClick;
    
           // 定义引发事件的方法
            public void OnDoubleClick(NewButtonClickEventArgs e) {
                EventHandler<NewButtonClickEventArgs> temp = Volatile.Read(ref DoubleClick);
                if (temp != null)
                {
                    temp(this, e);
                }
            }
        }
        class CallBacker{
            public CallBacker(Button btn){
                btn.DoubleClick += CallBack;
            }
            public void CallBack(Object sender,NewButtonClickEventArgs args){
                Console.WriteLine("按钮文本为:"+args.Text);
            }
        }

    2 揭秘事件

    为了弄清楚事件到底是什么,我们编译如下C#代码:

    namespace ConsoleApplication2
    {
        class Program
        {
            //定义委托
            delegate void MyDelegate(Object obj);
            //定义事件
            static event MyDelegate MyEvent;
    
            static void Main(string[] args)
            {
                MyEvent += Test1;//注册方法
                MyEvent += Test2;//注册方法
    
                MyEvent(new Object());//调用
                Console.ReadLine();
            }
            static void Test1(Object obj) {
                Console.WriteLine("test1");
            }
            static void Test2(Object obj) {
                Console.WriteLine("test2");
            }
        }
    }

    我们在编译上面的C#代码后,用ildasm工具打开它,可以看到如下这样:

    除了所定义的成员,还多了一个类(MyDelegate),一个字段(MyEvent),两个方法(add_MyEvent、remove_MyEvent)。其中类是由委托转化而来,这里不做详细参数,详情可以参见C#详解委托。

    一个事件的声明是可以转化为一个代理字段的声明加上添加、删除两种方法的事件操作。上面的MyEvent事件与MyEvent字段、add_MyEvent方法、remove_MyEvent方法关联起来了。

    再打开MyEvent事件的IL的IL代码,可以看到出现这样

    可以看出,事件的addOn和removeOn分别被重定向到了类中的add_MyEvent和remove_MyEvent方法上。


    笔者认为之所以要利用代理字段,原因很有可能是CLS不直接支持事件参与运行,因为说到底,事件还是属于引用类型变量。

    3 显式实现事件

    3.1 为什么需要显式实现事件

    在最开始我们已经知道了事件是基于委托的,也就是说事件是对委托的封装,一个事件的底层肯定有一个委托列表做支撑。
    在System.Windows.Forms.Control类型中定义了大约70个事件,

    假如Control类型在实现事件时,允许编译器生成add和remove访问器方法以及委托字段(每个事件都生成一个维护委托的委托列表),那么每个Control仅为事件就要多准备70个字段,这是非常浪费内存的。

    然而这种情况,是确实存在的。

    例如有如下代码:

    namespace ConsoleApplication2
    {
        class Program
        {
            //定义委托
            delegate void MyDelegate(Object obj);
            //定义事件
            static event MyDelegate MyEvent1;
            static event MyDelegate MyEvent2;
    
            static void Main(string[] args)
            {
    
            }
        }
    }

    编译后,再使用ildasm工具打开,可以看到如下情况:

    可以看出,我们定义了两个事件就出现了两个字段和四个方法,和上面对比不难发现,每当多定义一个事件,那么编译器就会为其新创建一个字段和两个方法。可想而知,如果定义70个事件会怎么样。

    如果定义一种事件能够被其他事件所共用就好了,接下来将讨论如何实现这个思路。

    3.2 显式实现事件的实现

    为了高效率的存储委托,公开了事件的每个对象都要维护一个集合(数据字典),集合将某种形式的事件标识符作为键(Key),将委托列表作为值(Value)。新对象构造时,这个集合是空白的。登记对一个事件的关注时,会在集合列表中查找该事件的标识符,如果存在这个标识符,就将新委托对象和旧委托对象合并。如果不存在,那么就添加当前委托对象和标识符到集合列表中。

    这样一来,我们就免去了自定义事件的步骤,按照上面的思想,将委托链表和某些键关联起来存储在集合中,当我们需要操作某些委托列表时,直接通过对应的键从集合列表中取出对应的委托链就可以了。这个过程未使用过事件,性能更高效。

        //在使用EventSet类时,作为Key使用。
        public sealed class EventKey { }
    
        public sealed class EventSet {
            //该字典用户维护 EventKey -> Delegate 的映射
            private readonly Dictionary<EventKey, Delegate> m_events = new Dictionary<EventKey, Delegate>();
    
            //添加 EventKey -> Delegate 的映射(如果不存在)
            //将新委托合并到旧委托中去(如果已经存在该EventKey的映射)
            public void Add(EventKey eventKey, Delegate handler) {
                Monitor.Enter(m_events);
                Delegate d;
                m_events.TryGetValue(eventKey,out d);
                m_events[eventKey] = Delegate.Combine(d,handler);
                Monitor.Exit(m_events);
            }
    
            //从eventKey映射的Delegate中删除hanlder委托
            //在删除最后一个委托后,同时删除 eventKey -> Delegate的映射
            public void remove(EventKey eventKey, Delegate handler) {
                Monitor.Enter(m_events);
                Delegate d;
                if (m_events.TryGetValue(eventKey, out d)) {
                    d = Delegate.Remove(d,handler);
    
                    if (d == null) { //没有委托了
                        m_events.Remove(eventKey);
                    }
                }
                Monitor.Exit(m_events);
            }
    
            //为指定eventKey映射的委托触发
            public void Raise(EventKey eventKey,Object sender,EventArgs e) {
                Monitor.Enter(m_events);
                Delegate d;
                m_events.TryGetValue(eventKey,out d);
                Monitor.Exit(m_events);
                if (d != null) {
                    //以对象数组的形式传递参数,如果参数不匹配DynamicInvoke会抛出异常。
                    d.DynamicInvoke(sender,e);
                }
            }
        }

    上面定义了两个类EventKey和EventSet,其中EventKey是用于维护EventSet的私有数据字典的(利用EventKey对象的Hash值),EventSet中定义了三个方法Add,Remove,Raise,这三个方法都利用Monitor类的同步访问对象机制来操作字典表。
    在定义好维护委托列表的类后,我们就可以按照如下的栗子来使用了:
     

       class FooEventArgs : EventArgs {
            
        }
        class TypeWithLotsOfEvents {
    
            private readonly EventSet m_eventSet = new EventSet();
            protected static readonly EventKey s_fooEventKey = new EventKey();
    
    
            //使派生类也能够访问
            protected EventSet EventSet
            {
                get{return m_eventSet;}
            }
    
    
            //定义事件访问器
            public event EventHandler<FooEventArgs> Foo {
                add { m_eventSet.Add(s_fooEventKey,value); }
                remove { m_eventSet.remove(s_fooEventKey, value); }
            }
    
            //定义触发事件的受保护的虚方法
            protected virtual void OnFoo(FooEventArgs e) {
                m_eventSet.Raise(s_fooEventKey,this,e);
            }
    
            //定义将输入转化为这个事件的方法
            public void SimulateFoo() {
                OnFoo(new FooEventArgs());
            }
        }

    调用代码:

        public sealed class Program {
            static void Main(String[] args) {
                TypeWithLotsOfEvents typeWithLotsOfEvents = new TypeWithLotsOfEvents();
                typeWithLotsOfEvents.Foo += HandlerFooEvent;
    
                typeWithLotsOfEvents.SimulateFoo();
            }
           static  void HandlerFooEvent(Object obj, FooEventArgs e) {
               Console.WriteLine("here arrived ...");
            }
        }






  • 相关阅读:
    如何修改容器内的/etc/resolv.conf
    OpenShift DNS的机制
    OpenShift 容器日志和应用日志分离问题
    python办公自动化(一)PPTX
    python装饰器 语法糖
    一步一步FLASK(一)
    linux python 安装 pymssql
    定制flask-admin的主页
    复制pycharm虚拟环境
    离线安装pycharm数据库驱动
  • 原文地址:https://www.cnblogs.com/HDK2016/p/9152958.html
Copyright © 2020-2023  润新知