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

2005年1月22日 18:37
Posted on 2005-11-07 08:36  miqier  阅读(238)  评论(0编辑  收藏  举报