.NET中事件实现机制(上)——使用委托和EventHandler

在“很久很久”以前,我对事件的印象就是:用户操作了某个控件、或者某个控件更改了某个状态,就会调用我写的代码。

现在我知道,事件实际上是委托的一种包装,不仅是控件,在程序的任何地方都有可能用到它(当然我仍然觉得事件机制是除了属性之外,提供给用户界面的一种好东西)。那么现在就来彻底弄懂它。

首先,在界面设计器上丢一个文本框,在属性页面的事件选择器中双击KeyPress,就会自动生成一个与之关联的“事件”:

        private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
        {
        }

这样,当用户在文本框中按下某个键时,就会触发这个方法,并且我们可以在参数e中得知用户具体按下的是哪个键,于是就可以做相应的处理。

这个东西是怎么实现的呢?我们打开窗体的designer文件,比如Form1.Designer.cs,找到textBox1的部分,其中增加了这一行代码:

this.textBox1.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.textBox1_KeyPress);

现在我们知道,Visual Studio实际上是向textBox1这个控件的KeyPress事件添加了一个“订阅”,而这个事件是一个KeyPressEventHandler类型,它的定义是:

public delegate void KeyPressEventHandler(object sender, KeyPressEventArgs e);

现在就来学习下怎么为自己的类添加一个事件,以方便其他的类添加或取消订阅,并在事件触发的时候回调其相应的方法吧。

按照M$推荐的方法,回调方法应该有两个参数:sender表示触发事件的具体对象,e表示事件触发的其他信息(比如上面的按键信息)。而e应该从EventArgs类继承。

比如我们要做一个Foo类,这个类每隔一定时间就向订阅者发送一些随机产生的垃圾信息。为此我们定义FooEventArgs:

    public class FooEventArgs : EventArgs
    {
        private string _message;

        public FooEventArgs(string message)
        {
            this._message = message;
        }

        public string Message { get { return _message; } }
    }

回调方法接受到这个对象时,就可以从Message属性取出Foo类所产生的垃圾信息。(备注:EventArgs类表示什么信息都没有,如果触发事件时不需要提供额外信息,比如button的Click事件,直接使用EventArgs表示空的事件参数即可。)

在Foo类的具体实现上,按照M$推荐做法,我们应该将事件定义成EventHandler的类型(类似地,如果我们的事件没有额外信息就不需要使用它的泛型类):

public event EventHandler<FooEventArgs> GeneratedRubbish;

至于EventHandler,它的定义是下面这样:

    [Serializable]
    public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

所以我们就明白了,它实际上是系统预先定义的一种委托,自动提供了sender和e两个参数。因此,回调方法必须也要接受这两个参数。比如我们有一个类要订阅这些垃圾信息,那么它所定义的回调方法就是下面这样:

        private void ReceivedEvent(object sender, FooEventArgs e)
        {
        }

添加订阅时(类似于Form1.Designer.cs中做的事),只需要添加关联即可:

t.GeneratedRubbish += ReceivedEvent;

那么Foo怎么触发这个事件呢?同样按照M$推荐的做法,我们需要定义一个OnXXXX的方法,这个方法接收FooEventArgs参数并回调任何已经订阅了这个事件的方法。按照事件的“定义”,只有定义事件的类才能触发,不过一般情况下子类应该也可以触发(比如Control的子类Button),所以这个方法通常是修饰成protected的。(如果不定义成protected,那么子类就没有办法触发或管理我们定义的GenerateRubbish事件,因为事件的触发和操作只能在定义的类中实现,其他的类只能订阅和取消订阅。)

我们编写OnGenerateRubbish方法如下:

        protected virtual void OnGenerateRubbish(FooEventArgs e)
        {
            if (GeneratedRubbish != null)
                GeneratedRubbish(this, e);
        }

这样,当GenerateRubbish有事件关联时,其所有订阅的方法都会按顺序被回调。

但这样会在多线程的环境下产生问题(一般都是多线程的),比如我们发现GeneratedRubbish不为null,但与此同时另一个订阅者正好移除了订阅,这样就会抛出NullReferenceException异常。

为了修正这个问题,我们可以简单地使用Interlocked类的CompareExchange将委托自动临时“存储”其他,这样就不会出现问题了:

        protected virtual void OnGenerateRubbish(FooEventArgs e)
        {
            EventHandler<FooEventArgs> temp = 
                Interlocked.CompareExchange(ref GeneratedRubbish, null, null);

            if (temp != null) temp(this, e);
        }

(备注:这样做的一个坏处是,假如发生上面那种冲突的情况,取消订阅后订阅者还是可能发生方法回调。)

这样,在Foo类及其子类的任何产生垃圾信息的地方,就可以调用OnGenerateRubbish来向订阅者“推送”垃圾信息。


写到这里,应该大概熟悉了事件的实现机制。但是我们发现EventHandler是系统预先定义好的,按理来说我们也可以不用那个东西自己用委托实现类似的功能。

那么就开始尝试吧。首先我们自己定义一个委托:

private delegate void HelloDelegate(Foo sender, string message);

然后再定义一个事件与之关联:

public event HelloDelegate GeneratedRubbish2;

那么OnGenerateRubbish事件按道理我们也可以使用完全相同的代码:

        protected virtual void OnGenerateRubbish2(string message)
        {
            HelloDelegate temp = Interlocked.CompareExchange(ref GeneratedRubbish2, null, null);

            if (temp != null) temp(this, message);
        }

这样是完全可行的,而且我们还摆脱了那个FooEventArgs类,回调方法的签名只接受string即可:

        private void ReceivedEvent2(Foo sender, string message)
        {
        }

甚至,我们可以完全不要sender,只要message都可以。


至此我们终于发现EventHandler只是系统提供的一种预定义的事件实现模式,我们也可以完全不管它按照自己想要的实现方式去进行。

不过,如果没有特殊要求,还是推荐大家按照M$的模式去实现事件,这样就比较规范,不会出现每个人实现事件时都按自己的喜好,从而一片混乱……

OK,这篇博文只是入门而已,介绍了事件最基本的实现方式。那么疑问就是,难道还有“高级”技巧?那么下一篇再说(玩下游戏再更新)。

✏️ 有任何想法?欢迎发邮件告诉老夫:daozhihun@outlook.com