通过前两节的学习,你已经掌握了 .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]
(我对 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]