通过前两节的学习,你已经掌握了 .NET
事件模型的原理和实现方式。这一节我将介绍两个替代方案,这些方案并不是推荐采用的,请尽量采用事件模型去实现。另外,在本节末尾,有一段适合熟悉 Java
语言的读者阅读,讨论了 .NET 和 Java 在“事件模型”方面的差异。
目录
使用接口实现回调
事件模型其实是回调函数的一种特例。像前面的例子,Form1 调用了 Worker,Worker 反过来(通过事件模型)让 Form1
改变了状态栏的信息。这个操作就属于回调的一种。
在“.NET Framework
类库设计指南”中提到了:“委托、接口和事件允许提供回调功能。每个类型都有自己特定的使用特性,使其更适合特定的情况。”(参见本地
SDK 版本,在线 MSDN 版本)
事件模型中,事实上也应用了委托来实现回调,可以说,事件模型是委托回调的一个特例。如果有机会,我会在关于多线程的教程中介绍委托回调在多线程中的应用。
这里我先来看看,如何使用接口实现回调功能,以达到前面事件模型实现的效果。
Demo 1I:使用接口实现回调。
using System;
using System.Threading;
using System.Collections;
namespace percyboy.EventModelDemo.Demo1I
{
// 注意这个接口
public interface IWorkerReport
{
void OnStartWork(int totalUnits);
void OnEndWork();
void OnRateReport(double rate);
}
public class Worker
{
private const int MAX = Consts.MAX;
private IWorkerReport report = null;
public Worker()
{
}
// 初始化时同时指定 IWorkerReport
public Worker(IWorkerReport report)
{
this.report = report;
}
// 或者初始化后,通过设置此属性指定
public IWorkerReport Report
{
set { report = value; }
}
public void DoLongTimeTask()
{
int i;
bool t = false;
double rate;
if (report != null)
{
report.OnStartWork( MAX );
}
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
rate = (double)i / (double)MAX;
if (report != null)
{
report.OnRateReport( rate );
}
}
if ( report != null)
{
report.OnEndWork();
}
}
}
}
你可以运行编译好的示例,它可以完成和前面介绍的事件模型一样的工作,并保证了耦合度没有增加。调用 Worker 的 Form1 需要做一个
IWorkerReport 的实现:
private void button1_Click(object sender, System.EventArgs e)
{
statusBar1.Text = "开始工作 ....";
this.Cursor = Cursors.WaitCursor;
long tick = DateTime.Now.Ticks;
Worker worker = new Worker();
// 指定 IWorkerReport
worker.Report = new MyWorkerReport(this);
worker.DoLongTimeTask();
tick = DateTime.Now.Ticks - tick;
TimeSpan ts = new TimeSpan(tick);
this.Cursor = Cursors.Default;
statusBar1.Text = String.Format("任务完成,耗时 {0} 秒。", ts.TotalSeconds);
}
// 这里实现 IWorkerReport
private class MyWorkerReport : IWorkerReport
{
public void OnStartWork(int totalUnits)
{
}
public void OnEndWork()
{
}
public void OnRateReport(double rate)
{
parent.statusBar1.Text = String.Format("已完成 {0:P0} ....", rate);
}
private Form1 parent;
public MyWorkerReport(Form1 form)
{
this.parent = form;
}
}
你或许已经觉得这种实现方式,虽然 Worker
类“里面”可能少了一些代码,却在调用时增加了很多代码量。从重复使用的角度来看,事件模型显然要更方便调用。另外,从面向对象的角度,我觉得理解了事件模型的原理之后,你会觉得“事件”会更亲切一些。
另外,IWorkerReport 中包含多个方法,而大多时候我们并不是每个方法都需要,就像上面的例子中那样,OnStartWork 和 OnEndWork
这两个都是空白。如果接口中的方法很多,也会给调用方增加更多的代码量。
下载的源代码中还包括一个 Demo 1J,它和 Worker 类一起,提供了一个 IWorkerReport 的默认实现
WorkerReportAdapter(每个方法都是空白)。这样,调用方只需要从 WorkerReportAdapter
继承,重写其中需要重写的方法,这样会减少一部分代码量。但我觉得仍然是很多。
注意,上述的代码,套用(仅仅是套用,因为它不是事件模型)“单播事件”和“多播事件”的概念来说,它只能支持“单播事件”。如果你想支持“多播事件”,我想你可以考虑加入
AddWorkerReport 和 RemoveWorkerReport 方法,并使用 Hashtable 等数据结构,存储每一个加入的
IWorkerReport。
[TOP]
.NET 事件模型和 Java 事件模型的对比
(我对 Java 语言的了解不是很多,如果有误,欢迎指正!)
.NET 的事件模型,对于 C#/VB.NET 两种主流语言来说,是在语言层次上实现的。C# 提供了 event 关键字,VB.NET 提供了
Event,RaiseEvent 关键字。像前面两节所讲的那样,它们都有各自的声明事件成员的语法。而 Java 语言本身是没有“事件”这一概念的。
从面向对象理论来看,.NET 的一个类(或类的实例:对象),可以拥有:字段、属性、方法、事件、构造函数、析构函数、运算符等成员类型。在 Java
中,类只有:字段、方法、构造函数、析构函数、运算符。Java 的类中没有属性和事件的概念。(虽然 Java Bean 中将 getWidth、setWidth
的两个方法,间接的转换为一个 Width 属性,但 Java 依然没有把“属性”作为一个语言层次的概念提出。)总之,在语言层次上,Java 不支持事件。
Java Swing 是 Java 世界中常用的制作 Windows 窗体程序的一套 API。在 Java Swing
中有一套事件模型,来让它的控件(比如 Button 等)拥有事件机制。
Swing 事件模型,有些类似于本节中介绍的接口机制。它使用的接口,诸如
ActionListener、KeyListener、MouseListener(注意:按照 Java 的命名习惯,接口命名不用前缀
I)等;它同时也提供一些接口的默认实现,如 KeyAdapter,MouseAdapter 等,使用方法大概和本节介绍的类似,它使用的是
addActionListener/removeActionListener,addKeyListener/removeKeyListener,addMouseListener/removeMouseListener
等方法,来增减这些接口的。
正像本节的例子那样,使用接口机制的 Swing 事件模型,需要书写很多的代码去实现接口或者重写 Adapter。而相比之下,.NET
事件模型则显得更为轻量级,所需的挂接代码仅一行足矣。
另一方面,我们看到 Swing 的命名方式,将这些接口都命名为 Listener,监听器;而相比之下,.NET 事件模型中,对事件的处理被称为
handler,事件处理程序。一个采用“监听”,一个是“处理”,我认为这体现了一种思维上的差异。
还拿张三大叫的例子来讲,“处理”模型是说:当张三大叫事件发生时,外界对它做出处理动作(handle this
event);监听,则是外界一直“监听”着张三的一举一动(listening),一旦张三大叫,监听器就被触发。处理模型是以张三为中心的思维,监听模型则是以外部环境为中心的思维。
[TOP]
目录
属性样式的事件声明
在第一节中,我们讨论了 .NET 事件模型的基本实现方式。这一部分我们将学习 C# 语言提供的高级实现方式:使用 add/remove
访问器声明事件。(注:本节内容不适用于 VB.NET。)
我们再来看看上一节中我们声明事件的格式:
public event [委托类型] [事件名称];
这种声明方法,类似于类中的字段(field)。无论是否有事件处理程序挂接,它都会占用一定的内存空间。一般情况中,这样的内存消耗或许是微不足道的;然而,还是有些时候,内存开销会变得不可接受。比如,类似
System.Windows.Forms.Control
类型具有五六十个事件,这些事件并非每次都会挂接事件处理程序,如果每次都无端的多处这么多的内存开销,可能就无法容忍了。
好在 C# 语言提供了“属性”样式的事件声明方式:
public event [委托类型] [事件名称]
{
add { .... }
remove { .... }
}
如上的格式声明事件,具有 add 和 remove 访问器,看起来就像属性声明中的 get 和 set 访问器。使用特定的存储方式(比如使用
Hashtable 等集合结构),通过 add 和 remove 访问器,自定义你自己的事件处理程序添加和移除的实现方法。
Demo 1G:“属性”样式的事件声明。我首先给出一种实现方案如下(此实现参考了 .NET Framework SDK
文档中的一些提示)(限于篇幅,我只将主要的部分贴在这里):
public delegate void StartWorkEventHandler(object sender, StartWorkEventArgs e);
public delegate void RateReportEventHandler(object sender, RateReportEventArgs e);
// 注意:本例中的实现,仅支持“单播事件”。
// 如需要“多播事件”支持,请参考 Demo 1H 的实现。
// 为每种事件生成一个唯一的 object 作为键
static readonly object StartWorkEventKey = new object();
static readonly object EndWorkEventKey = new object();
static readonly object RateReportEventKey = new object();
// 使用 Hashtable 存储事件处理程序
private Hashtable handlers = new Hashtable();
// 使用 protected 方法而没有直接将 handlers.Add / handlers.Remove
// 写入事件 add / remove 访问器,是因为:
// 如果 Worker 具有子类的话,
// 我们不希望子类可以直接访问、修改 handlers 这个 Hashtable。
// 并且,子类如果有其他的事件定义,
// 也可以使用基类的这几个方法方便的增减事件处理程序。
protected void AddEventHandler(object eventKey, Delegate handler)
{
lock(this)
{
if (handlers[ eventKey ] == null)
{
handlers.Add( eventKey, handler );
}
else
{
handlers[ eventKey ] = handler;
}
}
}
protected void RemoveEventHandler(object eventKey)
{
lock(this)
{
handlers.Remove( eventKey );
}
}
protected Delegate GetEventHandler(object eventKey)
{
return (Delegate) handlers[ eventKey ];
}
// 使用了 add 和 remove 访问器的事件声明
public event StartWorkEventHandler StartWork
{
add { AddEventHandler(StartWorkEventKey, value); }
remove { RemoveEventHandler(StartWorkEventKey); }
}
public event EventHandler EndWork
{
add { AddEventHandler(EndWorkEventKey, value); }
remove { RemoveEventHandler(EndWorkEventKey); }
}
public event RateReportEventHandler RateReport
{
add { AddEventHandler(RateReportEventKey, value); }
remove { RemoveEventHandler(RateReportEventKey); }
}
// 此处需要做些相应调整
protected virtual void OnStartWork( StartWorkEventArgs e )
{
StartWorkEventHandler handler =
(StartWorkEventHandler) GetEventHandler( StartWorkEventKey );
if (handler != null)
{
handler(this, e);
}
}
protected virtual void OnEndWork( EventArgs e )
{
EventHandler handler =
(EventHandler) GetEventHandler( EndWorkEventKey );
if (handler != null)
{
handler(this, e);
}
}
protected virtual void OnRateReport( RateReportEventArgs e )
{
RateReportEventHandler handler =
(RateReportEventHandler) GetEventHandler( RateReportEventKey );
if (handler != null)
{
handler(this, e);
}
}
public Worker()
{
}
public void DoLongTimeTask()
{
int i;
bool t = false;
double rate;
OnStartWork(new StartWorkEventArgs(MAX) );
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
rate = (double)i / (double)MAX;
OnRateReport( new RateReportEventArgs(rate) );
}
OnEndWork( EventArgs.Empty );
}
细细研读这段代码,不难理解它的算法。这里,使用了名为 handlers 的 Hashtable
存储外部挂接上的事件处理程序。每当事件处理程序被“add”,就把它加入到 handlers 里存储;相反 remove 时,就将它从 handlers
里移除。这里取 event 的 key (开始部分为每一种 event 都生成了一个 object 作为代表这种 event 的 key)作为
Hashtable 的键。
[TOP]
单播事件和多播事件
在 Demo 1G
给出的解决方案中,你或许已经注意到:如果某一事件被挂接多次,则后挂接的事件处理程序,将改写先挂接的事件处理程序。这里就涉及到一个概念,叫“单播事件”。
所谓单播事件,就是对象(类)发出的事件通知,只能被外界的某一个事件处理程序处理,而不能被多个事件处理程序处理。也就是说,此事件只能被挂接一次,它只能“传播”到一个地方。相对的,就有“多播事件”,对象(类)发出的事件通知,可以同时被外界不同的事件处理程序处理。
打个比方,上一节开头时张三大叫一声之后,既招来了救护车,也招来了警察叔叔(问他是不是回不了家了),或许还有电视转播车(现场直播、采访张三为什么大叫,呵呵)。
多播事件会有很多特殊的用法。如果以后有机会向大家介绍 Observer 模式,可以看看 Observer
模式中是怎么运用多播事件的。(注:经我初步测试,字段形式的事件声明,默认是支持“多播事件”的。所以如果在事件种类不多时,我建议你采用上一节中所讲的字段形式的声明方式。)
[TOP]
支持多播事件的改进
Demo1H,支持多播事件。为了支持多播事件,我们需要改进存储结构,请参考下面的算法:
public delegate void StartWorkEventHandler(object sender, StartWorkEventArgs e);
public delegate void RateReportEventHandler(object sender, RateReportEventArgs e);
// 为每种事件生成一个唯一的键
static readonly object StartWorkEventKey = new object();
static readonly object EndWorkEventKey = new object();
static readonly object RateReportEventKey = new object();
// 为外部挂接的每一个事件处理程序,生成一个唯一的键
private object EventHandlerKey
{
get { return new object(); }
}
// 对比 Demo 1G,
// 为了支持“多播”,
// 这里使用两个 Hashtable:一个记录 handlers,
// 另一个记录这些 handler 分别对应的 event 类型(event 的类型用各自不同的 eventKey 来表示)。
// 两个 Hashtable 都使用 handlerKey 作为键。
// 使用 Hashtable 存储事件处理程序
private Hashtable handlers = new Hashtable();
// 另一个 Hashtable 存储这些 handler 对应的事件类型
private Hashtable events = new Hashtable();
protected void AddEventHandler(object eventKey, Delegate handler)
{
// 注意添加时,首先取了一个 object 作为 handler 的 key,
// 并分别作为两个 Hashtable 的键。
lock(this)
{
object handlerKey = EventHandlerKey;
handlers.Add( handlerKey, handler );
events.Add( handlerKey, eventKey);
}
}
protected void RemoveEventHandler(object eventKey, Delegate handler)
{
// 移除时,遍历 events,对每一个符合 eventKey 的项,
// 分别检查其在 handlers 中的对应项,
// 如果两者都吻合,同时移除 events 和 handlers 中的对应项。
//
// 或许还有更简单的算法,不过我一时想不出来了 :(
lock(this)
{
foreach ( object handlerKey in events.Keys)
{
if (events[ handlerKey ] == eventKey)
{
if ( (Delegate)handlers[ handlerKey ] == handler )
{
handlers.Remove( handlers[ handlerKey ] );
events.Remove( events[ handlerKey ] );
break;
}
}
}
}
}
protected ArrayList GetEventHandlers(object eventKey)
{
ArrayList t = new ArrayList();
lock(this)
{
foreach ( object handlerKey in events.Keys )
{
if ( events[ handlerKey ] == eventKey)
{
t.Add( handlers[ handlerKey ] );
}
}
}
return t;
}
// 使用了 add 和 remove 访问器的事件声明
public event StartWorkEventHandler StartWork
{
add { AddEventHandler(StartWorkEventKey, value); }
remove { RemoveEventHandler(StartWorkEventKey, value); }
}
public event EventHandler EndWork
{
add { AddEventHandler(EndWorkEventKey, value); }
remove { RemoveEventHandler(EndWorkEventKey, value); }
}
public event RateReportEventHandler RateReport
{
add { AddEventHandler(RateReportEventKey, value); }
remove { RemoveEventHandler(RateReportEventKey, value); }
}
// 此处需要做些相应调整
protected virtual void OnStartWork( StartWorkEventArgs e )
{
ArrayList handlers = GetEventHandlers( StartWorkEventKey );
foreach(StartWorkEventHandler handler in handlers)
{
handler(this, e);
}
}
protected virtual void OnEndWork( EventArgs e )
{
ArrayList handlers = GetEventHandlers( EndWorkEventKey );
foreach(EventHandler handler in handlers)
{
handler(this, e);
}
}
protected virtual void OnRateReport( RateReportEventArgs e )
{
ArrayList handlers = GetEventHandlers( RateReportEventKey );
foreach(RateReportEventHandler handler in handlers)
{
handler(this, e);
}
}
上面给出的算法,只是给你做参考,应该还有比这个实现更简单、更高效的方式。
为了实现“多播事件”,这次使用了两个 Hashtable:一个存储“handlerKey - handler”对,一个存储“handlerKey -
eventKey”对。相信通过仔细研读,你可以读懂这段代码。我就不再赘述了。
[TOP]
目录
事件、事件处理程序概念
在面向对象理论中,一个对象(类的实例)可以有属性(property,获取或设置对象的状态)、方法(method,对象可以做的动作)等成员外,还有事件(event)。所谓事件,是对象内部状态发生了某些变化、或者对象做某些动作时(或做之前、做之后),向外界发出的通知。打个比方就是,对象“张三”肚子疼了,然后他站在空地上大叫一声“我肚子疼了!”事件就是这个通知。
那么,相对于对象内部发出的事件通知,外部环境可能需要应对某些事件的发生,而做出相应的反应。接着上面的比方,张三大叫一声之后,救护车来了把它接到医院(或者疯人院,呵呵,开个玩笑)。外界因应事件发生而做出的反应(具体到程序上,就是针对该事件而写的那些处理代码),称为事件处理程序(event
handler)。
事件处理程序必须和对象的事件挂钩后,才可能会被执行。否则,孤立的事件处理程序不会被执行。另一方面,对象发生事件时,并不一定要有相应的处理程序。就如张三大叫之后,外界环境没有做出任何反应。也就是说,对象的事件和外界对该对象的事件处理之间,并没有必然的联系,需要你去挂接。
在开始学习之前,我希望大家首先区分“事件”和“事件处理程序”这两个概念。事件是隶属于对象(类)本身的,事件处理程序是外界代码针对对象的事件做出的反应。事件,是对象(类)的设计者、开发者应该完成的;事件处理程序是外界调用方需要完成的。简单的说,事件是“内”;事件处理程序是“外”。
了解以上基本概念之后,我们开始学习具体的代码实现过程。因为涉及代码比较多,限于篇幅,我只是将代码中比较重要的部分贴在文章里,进行解析,剩余代码还是请读者自己查阅,我已经把源代码打了包提供下载。我也建议你对照这些源代码,来学习教程。[下载本教程的源代码]
[TOP]
问题描述:一个需要较长时间才能完成的任务
Demo 1A,问题描述。这是一个情景演示,也是本教程中其他 Demo 都致力于解决的一个“实际问题”:Worker
类中有一个可能需要较长时间才能完成的方法 DoLongTimeTask:
using System;
using System.Threading;
namespace percyboy.EventModelDemo.Demo1A
{
// 需要做很长时间才能完成任务的 Worker,没有加入任何汇报途径。
public class Worker
{
// 请根据你的机器配置情况,设置 MAX 的值。
// 在我这里(CPU: AMD Sempron 2400+, DDRAM 512MB)
// 当 MAX = 10000,任务耗时 20 秒。
private const int MAX = 10000;
public Worker()
{
}
public void DoLongTimeTask()
{
int i;
bool t = false;
for (i = 0; i <= MAX; i++)
{
// 此处 Thread.Sleep 的目的有两个:
// 一个是不让 CPU 时间全部耗费在这个任务上:
// 因为本例中的工作是一个纯粹消耗 CPU 计算资源的任务。
// 如果一直让它一直占用 CPU,则 CPU 时间几乎全部都耗费于此。
// 如果任务时间较短,可能影响不大;
// 但如果任务耗时也长,就可能会影响系统中其他任务的正常运行。
// 所以,Sleep 就是要让 CPU 有机会“分一下心”,
// 处理一下来自其他任务的计算请求。
//
// 当然,这里的主要目的是为了让这个任务看起来耗时更长一点。
Thread.Sleep(1);
t = !t;
}
}
}
}
界面很简单(本教程中其他 Demo 也都沿用这个界面,因为我们主要的研究对象是 Worker.cs):

单击“Start”按钮后,开始执行该方法。(具体的机器配置条件,完成此任务需要的时间也不同,你可以根据你的实际情况调整代码中的 MAX 值。)
在没有进度指示的情况下,界面长时间的无响应,往往会被用户认为是程序故障或者“死机”,而实际上,你的工作正在进行还没有结束。此次教程就是以解决此问题为实例,向你介绍
.NET 中事件模型的原理、设计与具体编码实现。
[TOP]
高耦合的实现
Demo 1B,高度耦合。有很多办法可以让 Worker 在工作的时候向用户界面报告进度,比如最容易想到的:
public void DoLongTimeTask()
{
int i;
bool t = false;
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
在此处书写刷新用户界面状态栏的代码
}
}
如果说 DoLongTimeTask 是用户界面(Windows 窗体)的一个方法,那么上面蓝色部分或许很简单,可能只不过是如下的两行代码:
double rate = (double)i / (double)MAX;
this.statusbar.Text = String.Format(@"已完成 {0:P2} ...", rate);
不过这样的话,DoLongTimeTask 就是这个 Windows 窗体的一部分了,显然它不利于其他窗体调用这段代码。那么:Worker
类应该作为一个相对独立的部分存在。源代码 Demo1B 中给出了这样的一个示例(应该还有很多种、和它类似的方法):
Windows 窗体 Form1 中单击“Start”按钮后,初始化 Worker 类的一个新实例,并执行它的 DoLongTimeTask
方法。但你应该同时看到,Form1 也赋值给 Worker 的一个属性,在 Worker 执行 DoLongTimeTask 方法时,通过这个属性刷新
Form1 的状态栏。Form1 和 Worker 之间相互粘在一起:Form1 依赖于 Worker 类(因为它单击按钮后要实例化
Worker),Worker 类也依赖于 Form1(因为它在工作时,需要访问 Form1)。这二者之间形成了高度耦合。
高度耦合同样不利于代码重用,你仍然无法在另一个窗体里使用 Worker 类,代码灵活度大为降低。正确的设计原则应该是努力实现低耦合:如果 Form1
必须依赖于 Worker 类,那么 Worker 类就不应该再反过来依赖于 Form1。
下面我们考虑使用 .NET 事件模型解决上述的“高度耦合”问题:
让 Worker 类在工作时,向外界发出“进度报告”的事件通知(RateReport)。同时,为了演示更多的情景,我们让 Worker
类在开始 DoLongTimeTask 之前发出一个“我要开始干活了!总任务数有 N 件。”的事件通知(StartWork),并在完成任务时发出“任务完成”的事件通知(EndWork)。
采用事件模型后,类 Worker 本身并不实际去刷新 Form1 的状态栏,也就是说 Worker 不依赖于 Form1。在 Form1
中,单击“Start”按钮后,Worker 的一个实例开始工作,并发出一系列的事件通知。我们需要做的是为 Worker
的事件书写事件处理程序,并将它们挂接起来。
[TOP]
事件模型的解决方案,简单易懂的 VB.NET 版本
Demo 1C,VB.NET 代码。虽然本教程以 C# 为示例语言,我还是给出一段 VB.NET 的代码辅助大家的理解。因为我个人认为
VB.NET 的事件语法,能让你非常直观的领悟到 .NET 事件模型的“思维方式”:
Public Class Worker
Private Const MAX = 10000
Public Sub New()
End Sub
' 注:此例的写法不符合 .NET Framework 类库设计指南中的约定,
' 只是为了让你快速理解事件模型而简化的。
' 请继续阅读,使用 Demo 1F 的 VB.NET 标准写法。
'
' 工作开始事件,并同时通知外界需要完成的数量。
Public Event StartWork(ByVal totalUnits As Integer)
' 进度汇报事件,通知外界任务完成的进度情况。
Public Event RateReport(ByVal rate As Double)
' 工作结束事件。
Public Event EndWork()
Public Sub DoLongTimeTask()
Dim i As Integer
Dim t As Boolean = False
Dim rate As Double
' 开始工作前,向外界发出事件通知
RaiseEvent StartWork(MAX)
For i = 0 To MAX
Thread.Sleep(1)
t = Not t
rate = i / MAX
RaiseEvent RateReport(rate)
Next
RaiseEvent EndWork()
End Sub
首先是事件的声明部分:你只需写上 Public Event 关键字,然后写事件的名称,后面的参数部分写上需要发送到外界的参数声明。
然后请注意已标记为蓝色的 RaiseEvent 关键字,VB.NET
使用此关键字在类内部引发事件,也就是向外界发送事件通知。请注意它的语法,RaiseEvent 后接上你要引发的事件名称,然后是具体的事件参数值。
从这个例子中,我们可以加深对事件模型的认识:事件是对象(类)的成员,在对象(类)内部状态发生了一些变化(比如此例中 rate
在变化),或者对象做一些动作时(比如此例中,方法开始时,向外界 raise event;方法结束时,向外界 raise
event),对象(类)发出的通知。并且,你也了解了事件参数的用法:事件参数是事件通知的相关内容,比如 RateReport 事件通知需要报告进度值
rate,StartWork 事件通知需要报告总任务数 MAX。
我想 RaiseEvent 很形象的说明了这些道理。
[TOP]
委托(delegate)简介。
在学习 C# 实现之前,我们首先应该了解一些关于“委托”的基础概念。
你可以简单的把“委托(delegate)”理解为 .NET
对函数的包装(这是委托的主要用途)。委托代表一“类”函数,它们都符合一定的规格,如:拥有相同的参数个数、参数类型、返回值类型等。也可以认为委托是对函数的抽象,是函数的“类”(类是具有某些相同特征的事物的抽象)。这时,委托的实例将代表一个具体的函数。
你可以用如下的方式声明委托:
public delegate void MyDelegate(int integerParameter);
如上的委托将可以用于代表:有且只有一个整数型参数、且不带返回值的一组函数。它的写法和一个函数的写法类似,只是多了 delegate
关键字、而没有函数体。(注:本文中的函数(function),取了面向过程理论中惯用的术语。在完全面向对象的 .NET/C#
中,我用以指代类的实例方法或静态方法(method),希望不会因此引起误解。顺带地,既然完全面向对象,其实委托本身也是一种对象。)
委托的实例化:既然委托是函数的“类”,那么使用委托之前也需要实例化。我们先看如下的代码:
public class Sample
{
public void DoSomething(int mode)
{
Console.WriteLine("test function.");
}
public static void Hello(int world)
{
Console.WriteLine("hello, world!");
}
}
我们看到 Sample 的实例方法 DoSomething 和静态方法 Hello 都符合上面已经定义了的 MyDelegate
委托的“规格”。那么我们可以使用 MyDelegate
委托来包装它们,以用于特殊的用途(比如下面要讲的事件模型,或者将来教程中要讲的多线程模型)。当然,包装的过程其实也是委托的实例化过程:
Sample sp = new Sample();
MyDelegate del = new MyDelegate(sp.DoSomething);
这是对上面的实例方法的包装。但如果这段代码写在 Sample 类内部,则应使用 this.DoSomething 而不用新建一个 Sample 实例。对
Sample 的 Hello 静态方法可以包装如下:
MyDelegate del = new MyDelegate(Sample.Hello);
调用委托:对于某个委托的实例(其实是一个具体的函数),如果想执行它:
del(12345);
直接写上委托实例的名字,并在括号中给相应的参数赋值即可。(如果函数有返回值,也可以像普通函数那样接收返回值)。
[TOP]
C# 实现
Demo 1D,C# 实现。这里给出 Demo 1C 中 VB.NET 代码的 C# 实现:是不是比 VB.NET 的代码复杂了一些呢?
using System;
using System.Threading;
namespace percyboy.EventModelDemo.Demo1D
{
// 需要做很长时间才能完成任务的 Worker,这次我们使用事件向外界通知进度。
public class Worker
{
private const int MAX = 10000;
// 注:此例的写法不符合 .NET Framework 类库设计指南中的约定,
// 只是为了让你快速理解事件模型而简化的。
// 请继续阅读,使用 Demo 1E / Demo 1H 的 C# 标准写法。
//
public delegate void StartWorkEventHandler(int totalUnits);
public delegate void EndWorkEventHandler();
public delegate void RateReportEventHandler(double rate);
public event StartWorkEventHandler StartWork;
public event EndWorkEventHandler EndWork;
public event RateReportEventHandler RateReport;
public Worker()
{
}
public void DoLongTimeTask()
{
int i;
bool t = false;
double rate;
if (StartWork != null)
{
StartWork(MAX);
}
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
rate = (double)i / (double)MAX;
if (RateReport != null)
{
RateReport(rate);
}
}
if (EndWork != null)
{
EndWork();
}
}
}
}
这份代码和上面 VB.NET 代码实现一致的功能。通过 C# 代码,我们可以看到被 VB.NET 隐藏了的一些实现细节:
首先,这里一开始声明了几个委托(delegate)。然后声明了三个事件,这里请注意 C# 事件声明的方法:
public event [委托类型] [事件名称];
这里你可以看到 VB.NET 隐藏了声明委托的步骤。
另外提醒你注意代码中具体引发事件的部分:
if (RateReport != null)
{
RateReport(rate);
}
在调用委托之前,必须检查委托是否为 null,否则将有可能引发 NullReferenceException 意外;比较 VB.NET
的代码,VB.NET 的 RaiseEvent 语句实际上也隐藏了这一细节。
好了,到此为止,Worker 类部分通过事件模型向外界发送事件通知的功能已经有了第一个版本,修改你的 Windows 窗体,给它添加 RateReport
事件处理程序(请参看你已下载的源代码),并挂接到一起,看看现在的效果:

添加了进度指示之后的界面,极大的改善了用户体验,对用户更为友好。
[TOP]
向“.NET Framework 类库设计指南”靠拢,标准实现
Demo 1E,C# 的标准实现。上文已经反复强调了 Demo 1C, Demo 1D 代码不符合 CLS 约定。微软为 .NET
类库的设计与命名提出了一些指南,作为一种约定,.NET 开发者应当遵守这些约定。涉及事件的部分,请参看事件命名指南(对应的在线网页),事件使用指南(对应的在线网页)。
using System;
using System.Threading;
namespace percyboy.EventModelDemo.Demo1E
{
public class Worker
{
private const int MAX = 10000;
public class StartWorkEventArgs : EventArgs
{
private int totalUnits;
public int TotalUnits
{
get { return totalUnits; }
}
public StartWorkEventArgs(int totalUnits)
{
this.totalUnits = totalUnits;
}
}
public class RateReportEventArgs : EventArgs
{
private double rate;
public double Rate
{
get { return rate; }
}
public RateReportEventArgs(double rate)
{
this.rate = rate;
}
}
public delegate void StartWorkEventHandler(object sender, StartWorkEventArgs e);
public delegate void RateReportEventHandler(object sender, RateReportEventArgs e);
public event StartWorkEventHandler StartWork;
public event EventHandler EndWork;
public event RateReportEventHandler RateReport;
protected virtual void OnStartWork( StartWorkEventArgs e )
{
if (StartWork != null)
{
StartWork(this, e);
}
}
protected virtual void OnEndWork( EventArgs e )
{
if (EndWork != null)
{
EndWork(this, e);
}
}
protected virtual void OnRateReport( RateReportEventArgs e )
{
if (RateReport != null)
{
RateReport(this, e);
}
}
public Worker()
{
}
public void DoLongTimeTask()
{
int i;
bool t = false;
double rate;
OnStartWork(new StartWorkEventArgs(MAX) );
for (i = 0; i <= MAX; i++)
{
Thread.Sleep(1);
t = !t;
rate = (double)i / (double)MAX;
OnRateReport( new RateReportEventArgs(rate) );
}
OnEndWork( EventArgs.Empty );
}
}
}
按照 .NET Framework 类库设计指南中的约定:
(1)事件委托名称应以 EventHandler 为结尾;
(2)事件委托的“规格”应该是两个参数:第一个参数是 object 类型的 sender,代表发出事件通知的对象(代码中一般是 this
关键字(VB.NET 中是 Me))。第二个参数 e,应该是 EventArgs 类型或者从 EventArgs 继承而来的类型;
事件参数类型,应从 EventArgs 继承,名称应以 EventArgs 结尾。应该将所有想通过事件、传达到外界的信息,放在事件参数 e 中。
(3)一般的,只要类不是密封(C# 中的 sealed,VB.NET 中的 NotInheritable)的,或者说此类可被继承,应该为每个事件提供一个
protected 并且是可重写(C# 用 virtual,VB.NET 用
Overridable)的
OnXxxx 方法:该方法名称,应该是 On 加上事件的名称;只有一个事件参数 e;一般在该方法中进行 null 判断,并且把 this/Me 作为
sender 执行事件委托;在需要发出事件通知的地方,应调用此 OnXxxx 方法。
对于此类的子类,如果要改变发生此事件时的行为,应重写 OnXxxx 方法;并且在重写时,一般情况下应调用基类的此方法(C# 里的
base.OnXxxx,VB.NET 用 MyBase.OnXxxx)。
我建议你能继续花些时间研究一下这份代码的写法,它是 C# 的标准事件实现代码,相信你会用得着它!
在 Demo 1D 中我没有讲解如何将事件处理程序挂接到 Worker 实例的事件的代码,在这个 Demo 中,我将主要的部分列在这里:
private void button1_Click(object sender, System.EventArgs e)
{
statusBar1.Text = "开始工作 ....";
this.Cursor = Cursors.WaitCursor;
long tick = DateTime.Now.Ticks;
Worker worker = new Worker();
// 将事件处理程序与 Worker 的相应事件挂钩
// 这里我只挂钩了 RateReport 事件做示意
worker.RateReport += new Worker.RateReportEventHandler(this.worker_RateReport);
worker.DoLongTimeTask();
tick = DateTime.Now.Ticks - tick;
TimeSpan ts = new TimeSpan(tick);
this.Cursor = Cursors.Default;
statusBar1.Text = String.Format("任务完成,耗时 {0} 秒。", ts.TotalSeconds);
}
private void worker_RateReport(object sender, Worker.RateReportEventArgs e)
{
this.statusBar1.Text = String.Format("已完成 {0:P0} ....", e.Rate);
}
请注意 C# 的挂接方式(“+=”运算符)。
到这里为此,你已经看到了事件机制的好处:Worker 类的代码和这个 Windows Form 没有依赖关系。Worker
类可以单独存在,可以被重复应用到不同的地方。
VB.NET 的读者,请查看 Demo 1F 中的 VB.NET 标准事件写法,并参考这里的说明,我就不再赘述了。
[TOP]