把委托说透(3):委托与事件

把委托说透(1)(2)中,先后介绍了委托的语法和本质,本文重点介绍.NET中与委托息息相关的概念——事件。在此之前,首先需要补充(2)中遗漏的一部分内容,即C#在语法上对委托链的支持。

C#编译器为委托类型提供了+=和-=两个操作符的重载,分别对应Delegate.Combine和Delegate.Remove方法,使用这两个操作符可以大大简化委托链的构造和移除。

好了,有了+=和-=,我们就可以开始今天的话题了。

什么是事件?

事件(event)是类型中的一种成员,定义了事件成员的类型允许类型(或者类型的实例)在某些特定事情发生的时候通知其他对象。如Button类型的Click事件,在按钮被点击的时候,程序中的其他对象可以得到一个通知,并执行相应的动作。事件就是支持这种交互的类型成员

CLR中的事件模型是建立在委托这一机制之上的,这种关联存在其必然性。

我们知道,委托是对方法的抽象,它将方法的调用与实现相分离。方法的调用者(即委托的执行者)并不知道方法的内部是如何实现的,而方法的实现者也不知道该方法会在何时被调用。

事件也是如此。事件被触发后会执行什么样的操作,是由触发者决定的,如点击一个按钮之后是插入一条记录还是用户登录。事件的拥有者只知道什么情况下会触发事件,但并不知道事件的具体实现。因此用委托来实现事件的机制就是自然而然的事情了。

事件与委托的关系到底是什么样呢?委托是与类、接口同一级别的概念,而事件属于类型的成员,与方法、属性、字段等是同一级别的概念。一个与事件相关联的委托的定义如下:

public delegate void FooEventHandler(object sender, FooEventArgs e);

而相应事件成员的定义为:

public event FooEventHandler Foo;

可见,事件用event关键字定义,其类型为一个委托类型,即事件是通过委托来实现的。

一个完整的事件定义和使用的例子如下:

public delegate void FooEventHandler(object sender, FooEventArgs e);public class FooEventArgs : EventArgs { }public class Bar{    public event FooEventHandler Foo;    protected virtual void OnFoo(FooEventArgs e)    {        FooEventHandler handler = Foo;        if (handler != null)            handler(this, e);    }    public void SomeMethod()    {        // ...        OnFoo(new FooEventArgs());        // ...    }}public class Client{    public Client()    {        Bar b = new Bar();        b.Foo += new FooEventHandler(b_Foo);    }    void b_Foo(object sender, FooEventArgs e)    {        throw new NotImplementedException();    }}

我们注意到在SomeMethod方法中并没有直接调用委托,而是调用了一个辅助方法OnFoo。在该方法中,先将Foo事件的引用传递给新定义的委托,然后再进行空判断,在委托不为null的情况下才进行调用。这样做是为了保证线程和类型的安全,我们在下面将会介绍。

还有一个需要注意的地方是,客户端为事件注册方法时,使用的是+=操作符。在本文开头已经介绍,+=对应Delegate.Combine方法,回顾(2)中阐述的委托链的构造,我们可以得出如下结论:在为事件注册方法时,实际上是在构造一个委托链

事件的设计规范

《Framework Design Guidelines 2nd Edition》一书应该成为我们设计.NET程序的规范手册。书中对于事件的定义采取了如下的规定:

事件的命名

由于通常事件以为着某种行为,因此事件的名称应该为一个动词,并用动词的时态来指明事件发生的时间。《Framework Design Guidelines 2nd Edition》对事件命名的建议如下:

1. 用动词或动词短语来为事件命名。如Clicked、Painting、DroppedDown等等。

2. 用现在时和将来时来表示“之前”和“之后”的概念,不要用Before和Arfter前缀。例如在窗体关闭之前触发的事件可以命名为Closing,而窗体关闭之后触发的事件则应该命名为Closed。

3. 为事件处理程序(委托)的名称添加EventHandler后缀。如

4. 使用sender和e来命名时间的两个参数。如上例。

5. 为事件的数据参数类型的名称添加EventArgs后缀。如上例。

事件的设计

1. 通常情况下,事件所对应的委托的返回值为void,并且包含两个参数:第一个参数为触发事件的对象,通常为事件的拥有者(即上例中的Bar对象)。第二个参数为事件相关的数据,由事件的拥有者传递给事件的调用者。

2. 在.NET 2.0及以后的版本中自定义事件时,使用System.EventHandler委托,而不要自定义新的委托类型。因此上例中如果在.NET 2.0下应该定义为:

public event EventHandler<FooEventArgs> Foo;

在.NET 2.0以前,由于不支持泛型,我们仍然需要像上面例子中那样定义。

3. 为事件自定义一个EventArgs的子类,作为传递数据的参数。如果不需要传递任何参数,可以直接使用EventArgs类。

4. 为每个事件编写一个受保护的虚方法作为触发方法,如上例中的OnFoo方法。这仅适用于unsealed类的非静态事件,并不适用于struct、sealed class和静态事件。这样做的原因是,通过override为子类提供一种处理事件的方式。按照惯例,该虚方法以On开头,以事件名称结尾,如OnFoo方法。

为了确保委托在调用时不抛出NullReferenceException,在OnXxx方法中通常都会对委托进行判空操作,如

if (Xxx != null) Xxx(this, e);

然而仅仅这样是不够的,因为事件处理程序的添加和移除并不是线程安全的,因此在多线程环境下,Xxx委托在判空之后很可能被Remove,导致Xxx在调用时可能为null。由于Remove方法将会构造一个新的委托实例,而不会改变原委托的引用,因此需要先将委托的引用传递给一个新的委托,再对这个新委托进行判空和调用等操作,这样即使原委托被Remove,也不会NullReferenceException。

FooEventHandler handler = Foo;if (handler != null) handler(this, e);

5. 触发事件的方法有且仅有一个参数,XxxEventArgs参数。

6. 在触发非静态事件时,sender参数不要为null。对于静态事件,sender参数要为null。

7. 触发事件时,如果不需要传递任何数据,数据参数可以为EventArgs.Empty,不要为null。

事件的应用举例

在前面随笔的评论中,有同学提出希望列举委托在窗体间传值的例子。好吧,我们就举一个简单的WinForm窗体传值的例子。

我们首先新建一个Windows From应用程序,并新建两个窗体MainForm和SubForm,在MainForm中建立两个Button,在SubForm中添加一个RichTextBox。如下图所示:

image image

当点击“开始”的时候,会弹出SubForm,点击“传值”的时候,会将当前时间显示在SubForm的RichTextBox中。

需求大体就是这样了,我们该如何设计呢?

点击“传值”按钮后,会引起SubForm的变化。SubForm只负责显示,它并不知道引起变化的原因。MainForm负责引起变化,并将变化传递给SubForm,但它并不关心SubForm如何进行处理。这与我们之前对事件的描述十分相似:

事件被触发后会执行什么样的操作,是由触发者决定的,如点击一个按钮之后是插入一条记录还是用户登录。事件的拥有者只知道什么情况下会触发事件,但并不知道事件的具体实现。

因此,在这个示例中,我们可以通过事件来实现传值。我们首先创建数据参数类SendEventArgs,它包含一个Message属性,用来保存数据。

public class SendEventArgs : EventArgs{    public string Message { get; private set; }    public SendEventArgs(string message)    {        this.Message = message;    }}

然后在MainForm中添加一个事件:Send。

public event EventHandler<SendEventArgs> Send;

然后我们为该事件编写触发方法OnSend:

protected virtual void OnSend(SendEventArgs e){    EventHandler<SendEventArgs> handler = Send;    if (handler != null)        handler(this, e);}

MainForm中两个按钮的事件处理程序如下:

private void btnBegin_Click(object sender, EventArgs e){    SubForm subForm = new SubForm(this);    subForm.Show();}private void btnSend_Click(object sender, EventArgs e){    SendEventArgs sendEventArgs = new SendEventArgs(DateTime.Now.ToString());    OnSend(sendEventArgs);}

btnBegin按钮用来打开一个SubForm,并将当前MainForm实例作为参数传入。btnSend按钮用来构造Send事件的数据参数,并调用Send事件的触发方法。

在SubForm中,有一个MainForm类型的私有字段,用于保存构造函数里传入的参数。

private MainForm parent;

构造函数中除了给parent字段赋值外,还要注册parent的Send事件的处理程序:

public SubForm(MainForm main){    InitializeComponent();    this.parent = main;    parent.Send += new EventHandler<SendEventArgs>(parent_Send);}

parent_Send处理程序负责向RichTextBox中添加信息:

private void parent_Send(object sender, SendEventArgs e){    this.rtbTime.AppendText(e.Message);    this.rtbTime.AppendText(Environment.NewLine);}

最后我们在SubForm的Closing事件里移除parent_Send,这样就可以打开多个SubForm了。

private void SubForm_FormClosing(object sender, FormClosingEventArgs e){    parent.Send -= new EventHandler<SendEventArgs>(parent_Send);}

整个Demo的显示如下:

image

总结

本文重点讲解了.NET中的事件,并对事件的设计进行了规范,最终通过一个示例加深了我们对事件的理解。

您是否从以上示例中感觉到了观察者模式的影子呢?本系列接下来的一篇随笔中,我们将会讨论委托与设计模式的微妙联系。

NET技术把委托说透(3):委托与事件,转载需保留来源!

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。