C#详解事件

Posted hdk2016

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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工具打开它,可以看到如下这样:
技术分享图片
除了所定义的成员还多了一个类,一个字段,两个方法。其中类是委托,这里不详细阐述。

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

再打开MyEvent事件的IL的IL代码,可以看到出现这样
技术分享图片
可以看出,事件的addOn和removeOn分别被重定向到了类中的add_MyEvent和remove_MyEvent方法上。
笔者认为之所以要利用代理字段,原因很有可能是CLR不直接支持事件参与运行,因为说到底,事件还是属于引用类型变量。

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 ...");
        }
    }

 






以上是关于C#详解事件的主要内容,如果未能解决你的问题,请参考以下文章

c# winform_form属性详解以及事件详解(二)

C# 事件详解

C#入门详解(11)

C# VB6 OCX 生成的消费事件

C# 中 XBOX 控制器的事件处理程序?

C#入门详解笔记事件 泛型 委托 Lambda表达式 LINQ