艾伟_转载:把委托说透(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。如下图所示:

当点击“开始”的时候,会弹出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的显示如下:

总结

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

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

时间: 2024-09-15 11:53:29

艾伟_转载:把委托说透(3):委托与事件的相关文章

艾伟_转载:解惑答疑:C#委托和事件

相信很多人一直为C#委托和事件所困惑,尤其是C#的初学者,学到这一部分会感觉比较困难,很有可能就放弃了,而且.NET对委托和事件封装得挺好,一般都不怎么用到自定义的委托和事件,所以放弃学习该技术就有了借口! 网上也有不少此类的文章,最具代表性的是张子阳的C#中的委托和事件这篇,写得的确很好,得到很多读者的赞赏,但我 看评论,还是发现了些问题,因为有不少读者是看了一遍又一遍,每次感觉都蛮好,可是隔一段时间,对"委托和事件"又迷糊了,于是又来看!我真搞不懂,为什 么会出现这种情况!后来想想

艾伟_转载:把委托说透(2):深入理解委托

在上一篇随笔中我们通过示例逐步引入了委托,并比较了委托和接口.本文将重点剖析委托的实质. 委托在本质上仍然是一个类,我们用delegate关键字声明的所有委托都继承自System.MulticastDelegate.后者又是继承自System.Delegate类,System.Delegate类则继承自System.Object.委托既然是一个类,那么它就可以被定义在任何地方,即可以定义在类的内部,也可以定义在类的外部. 正如很多资料上所说的,委托是一种类型安全的函数回调机制, 它不仅能够调用实

艾伟_转载:把委托说透(1):开始委托之旅 委托与接口

委托,本是一个非常基础的.NET概念,但前一阵子在园子里却引起轩然大波.先是Michael Tao的随笔让人们将委托的写法与茴香豆联系到了一起,接着老赵又用一系列文章分析委托写法的演变,并告诫"嘲笑孔乙己的朋友们,你们在一味鄙视"茴"的四种写法的同时,说不定也失去了一个了解中国传统文化的机会呢!". 在我个人看来,委托是.NET Framework中一个非常炫的特性,绝不会向有些评论里说的那样,根本没有机会接触.恰恰相反,我们几乎每天都会接触委托,使用委托. 其实园

艾伟_转载:一个.NET委托的故事:彼得,老板和宇宙

紧耦合 从前,在南方一块奇异的土地上,有个工人名叫彼得,他非常勤奋,对他的老板总是百依百顺.但是他的老板是个吝啬的人,从不信任别人,坚决要求随时知道彼得的工作进度,以防止他偷懒.但是彼得又不想让老板呆在他的办公室里站在背后盯着他,于是就对老板做出承诺:无论何时,只要我的工作取得了一点进展我都会及时让你知道.彼得通过周期性地使用"带类型的引用"(原文为:"typed reference" 也就是delegate??)"回调"他的老板来实现他的承诺,

艾伟_转载:.NET委托:一个C#睡前故事

英文版原作者:Chris Sells(www.sellsbrothers.com) 翻译:袁晓辉(www.farproc.com http://blog.csdn.net/uoyevoli) 原文地址: http://dev.csdn.net/article/82/82644.shtm 紧耦合 从前,在南方一块奇异的土地上,有个工人名叫彼得,他非常勤奋,对他的老板总是百依百顺.但是他的老板是个吝啬的人,从不信任别人,坚决要求随时知道彼得的工作进度,以防止他偷懒.但是彼得又不想让老板呆在他的办公室

艾伟_转载:探索.Net中的委托

废话 我本来以为委托很简单,本来只想简简单单的说说委托背后的东西,委托的使用方法.原本只想解释一下那句:委托是面向对象的.类型安全的函数指针.可没想到最后惹出一堆的事情来,越惹越多,罪过,罪过.本文后面一部分是我在一边用SOS探索一边记录的,写的非常糟糕,希望您的慧眼能发现一些有价值的东西,那我就感到无比的荣幸了. 委托前世与今生 大家可能还记得,在C/C++里,我们可以在一个函数里实现一个算法的骨架,然后在这个函数的参数里放一个"钩子",使用的时候,利用这个"钩子"

艾伟_转载:把委托说透(4):委托与设计模式

委托与很多设计模式都有着千丝万缕的联系,在前面的随笔中已经介绍了委托与策略模式的联系,本节主要来讨论委托与其他两个模式:观察者模式和模板方法模式. 委托与观察者模式 在.NET中,很多设计模式得到了广泛应用,如foreach关键字实现了迭代器模式.同样的,.NET中也内置了观察者模式的实现方式,这种方式就是委托. 观察者模式的一般实现 网上可以找到很多资料介绍观察者模式的实现,我这里介绍一种简单的退化后的观察者模式,即Subject类为具体类,在其之上不再进行抽象. public class S

艾伟_转载:C# 委托的同步调用和异步调用

委托的Invoke方法用来进行同步调用.同步调用也可以叫阻塞调用,它将阻塞当前线程,然后执行调用,调用完毕后再继续向下进行. 同步调用的例子: using System;using System.Threading;public delegate int AddHandler(int a, int b);public class Foo {static void Main() { Console.WriteLine("**********SyncInvokeTest**************&

艾伟_转载:数组排序方法的性能比较(上):注意事项及试验

昨天有朋友写了一篇文章,其中比较了List的Sort方法与LINQ中排序方法的性能,而最终得到的结果是"LINQ排序方法性能高于List.Sort方法".这个结果不禁让我很疑惑.因为List.Sort方法是改变容器内部元素的顺序,而LINQ排序后得到的是一个新的序列.假如两个排序方法的算法完全一致,LINQ排序也比对方多出元素复制的开销,为什么性能反而会高?如果LINQ排序的算法/实现更为优秀,那为什么.NET Fx不将List.Sort也一并优化一下呢?于是今天我也对这个问题进行了简