代码改变世界

C#委托,事件(三)——事件

2010-05-15 13:49  杨平  阅读(1024)  评论(0编辑  收藏  举报

目录

  1. 事件概述
  2. 观察者模式

上节中,我们从委托独自一方面进行了探究,本节将从事件独自一方面进行探究,希望能给大家留下有价值的信息。

  • 事件概述

有过MFC开发的朋友都知道,在学习MFC入门时研究的Windows消息机制是这样的——首先应用程序从WinMan函数进入消息监听循环(消息监听器)中,如果用户触发了某事件,消息监听器就将消息路由到过程函数(消息处理函数)中,处理事件相关的逻辑。

这就好比在窗体应用程序中,主窗体有一个按钮Bt01,当用户点击该按钮时, 系统就将用户单击按钮的动作作为消息push到消息队列中等待处理,当时间片轮到该消息处理时,系统就会路由到应用程序中调用已注册的过程函数处理逻辑。此处的过程函数就是我们通常编写的事件处理方法。

在面向对象的编程中,事件模式就有些不一样了。.NET库将Windows消息封装到事件中了,这样,在两个对象之间就可以通过事件来相互通信了,而委托就用作封装事件处理方法上,简单点,就是事件处理函数的注册。

  • 观察者模式

个人认为,该模式能清晰的描述事件的工作模式,下面就以烧水为例来进行详细的讲述。

现在,在烧水过程中主要有以下几个步骤:1.烧水,2.当水快烧开时鸣笛提示,3.鸣笛30秒后自动切断电源停止烧水,防止将水烧干。

因此,我们可以这样来设计代码:

    public class Heater
    {        
        private int timer; //水烧开后,鸣笛计时
        private Boolean boiling; //烧水开关

        /// <summary>
        /// 水壶烧水
        /// </summary>
        public void BoilWater()
        {
            int temp = 0;

            boiling = true;
            while (boiling)
            {
                if(temp <= 100)
                {
                    temp++;
                }
                else
                {                    
                    AlertTip();
                }

                if (timer == 30)
                {
                    
                    ShutDown();
                }
            }
        }

        /// <summary>
        /// 鸣笛警报,并计时
        /// </summary>
        private void AlertTip()
        {
            timer++;
            Console.WriteLine("Alarm:水快烧开了!");
        }

        /// <summary>
        /// 自动关闭,停止烧水
        /// </summary>
        private void ShutDown()
        {
            boiling = false;
            Console.WriteLine("ShutDown:自动跳闸,停止烧水!");
        }
    }

    class Program
    {
        static void Main()
        {
            Heater ht = new Heater();
            ht.BoilWater();
        }
    }

上述代码中,我们定义了一个水壶类Heater,在Heater中有三个方法:BoilWater()——烧水;AlertTip()——水烧开了鸣笛提示;ShutDown()——鸣笛半分钟后自动跳闸停止烧水。这样的设计,就需要水壶生产商生产烧水设备和警报设备。但是,如果该水壶是由水壶生产商和警报器生产商生产的可跳闸水壶和警报器组装而成的,那又该怎样设置呢?上面的代码显然是不能满足需求了,此时就需要使用事件了。

首先,将上述中的部件模拟成各自的类型,修改后代码为:

    public class Heater
    {        
        public Boolean boiling; //烧水开关
        public event AlertEventHandle AlertEvent;        

        /// <summary>
        /// 水壶烧水
        /// </summary>
        public void BoilWater()
        {
            int temp = 0;

            boiling = true;
            while (boiling)
            {
                if(temp < 100)
                {
                    temp++;
                }
                else
                {
                    if (AlertEvent != null)
                        AlertEvent();
                }
            }
        }

        public void ShutDown() 
        {
            boiling = false;
            Console.WriteLine("ShutDown:自动跳闸,停止烧水!");
        }
    }

    public class Alert
    {
        public event ShutEventHandle ShutEvent;
        private int timer; //水烧开后,鸣笛计时

        public Alert() 
        {
            timer = 0;
        }
        /// <summary>
        /// 鸣笛警报,并计时
        /// </summary>
        public void AlertTip()
        {
            if (timer < 30)
            {
                timer++;
                Console.WriteLine("Alarm:水快烧开了!");
            }
            else
            {
                if (ShutEvent != null)
                    ShutEvent();
            }
            
        }
    }

 

并且,在该命名空间中,声明如下委托-事件处理的载体。代码如下:

    public delegate void AlertEventHandle(Heater par);
    public delegate void ShutEventHandle(Heater par);

当然,main函数中也要稍微改动下:

 class Program
    {
        static void Main()
        {
            Heater heater = new Heater();
            Alert alert = new Alert();
            AotuShut aotuShut = new AotuShut();

            heater.AlertEvent += alert.AlertTip;
            heater.ShutEvent += aotuShut.ShutDown;

            heater.BoilWater();
        }
    }

运行结果如下:

未命名

修改后的代码中,Heater类中声明了鸣笛事件AlertEvent;Alert类中同样也声明了自动跳闸事件ShutEvent。

我们来看看使用事件后,烧水是怎样进行的。在Main函数中,首先创建一个水壶对象,一个Alert(警报器)对象,然后分别将Alert类的成员方法AlertTip()——鸣笛——注册到水壶类对象和将水壶类的成员方法ShutDown()——自动跳闸——注册到警报器对象中。然后执行烧水动作。在烧水过程中,当水烧开时,Heater类向这里Alert类对象alert发送鸣笛事件通知执行鸣笛并计时;警报器鸣笛30秒后通知水壶对象heater自动跳闸停止烧水。

全部代码:

 

其中包含了两组动作,其一是水壶通知警报器鸣笛提示,其二是警报器通知水壶跳闸停止烧水。而这两组动作中都存在这样两个角色——观察者和发送者。在水壶通知警报器鸣笛时,水壶是发送者,警报器是观察者;同理在同时跳闸时水壶是观察者,警报器是发送者。其实在C#中,事件的接收即观察者并不知道发送者的任何信息,这就必须使用委托将观察者和发送者联系起来,所以事件是委托类型的。

接下来,我们看看在Main函数中注册事件时使用的是“+=”,那我们为什么不用“=”呢?改变成“=”后就回收到编译错误:Heater.AlertEvent必须使用的+=或-=的左右…

为什么?其实在声明事件时,虽然我们声明的是Public类型的,但系统在编译时还是将事件转换成了private私有类型,如上public event ShutEventHandle ShutEvent; 就回转化成private event ShutEventHandle ShutEvent;

同时还生成有如下代码:

    [MethodImpl(MethodImplOptions.Synchronized)]
    public void add_AlertEvent(AlertEventHandle value)
    {
        this.AlertEvent = (AlertEventHandle) Delegate.Combine(this.AlertEvent, value);
    }

    [MethodImpl(MethodImplOptions.Synchronized)]
    public void remove_AlertEvent(AlertEventHandle value)
    {
        this.AlertEvent = (AlertEventHandle) Delegate.Remove(this.AlertEvent, value);
    }

这样,事件就不能用“=”号来赋值了,使用“+=”和“-=”就相当于调用了上述add_AlertEvent()和remove_AlertEvent()方法,这两个方法是将事件处理函数添加到委托链或从委托链中删除。


总结:

说了一箩筐,虽然不是从事件的概念等教条来规矩的论述论点,但也从侧面以一示例描述了事件的工作原理,其实.NET中有很多的约定,比如声明事件的处理函数时方法名就是在事件名前加上"On"。正是有了这些约定,我们才能编写出更加优质的代码。

C#的事件和委托,是初学者的一道门槛,只要跨过了这道门槛再回头体味委托和事件时,才发现其实很简单。