C#中事件

一、事件基础

1、事件简介
  • 事件是在委托类型变量前加上event关键字,其本质是用来对委托类型的变量进行封装,类似于类的属性字段的封装。
  • 事件相当于增强了委托的封装性,以保证委托类型的变量在类外部不能被直接调用。这样相当于无论是在类的内部声明public还是protected的委托类型变量,只要用事件event进行封装,其效果都相当于声明了一个私有的委托类型变量。
    //委托的声明
    public delegate void TheDelegate(int num);

    class Test
    {
        //在类中声明委托类型的变量,其为公有的
        public TheDelegate delegate00;
        //在类中声明事件,即在委托类型变量前加上event关键字,其等价于声明一个私有的委托类型变量如delegate02
        public event TheDelegate delegate01;

        private TheDelegate delegate02;

        public void func01(int num01)
        {
            Console.WriteLine("This is func01");
        }

        public void func02(int num02)
        {
            Console.WriteLine("This is func02");
        }

    }
2、事件与委托的区别
  • 在注册和注销事件上,委托可以使用=+=来将函数注册到委托的变量上,使用-=来将函数注销。而事件则有着更严格的限制,事件只能使用+=来将函数注册到其上,使用-=来将函数注销。
  • 事件类型的委托变量(即用事件封装的委托类型变量),在外部不能直接当成其引用的函数来进行调用,因为其相当于私有的委托类型变量,但特殊在于它在外部只能用于注册或注销函数,而公有的委托类型在外部是可以直接用来调用对应的注册函数。
  • 在此,事件私有类型的委托变量的区别就在于:私有类型的委托变量在类外部是无法直接访问的,而事件相对于私有类型的委托变量又稍微多了一点点特例,就是事件可以在类外部被直接访问,但只能放在左边用来注册或注销函数。除此之外,它们用法相同,如都不能在类外部直接调用对应的注册函数等。如下图在类外部的主函数中调用示例:
    在这里插入图片描述
3、VS中事件的提示及查看

在输入对象加点号后,VS会出现智能提示,其分别代表:

  • 小扳手:表示属性
  • 立方体:表示函数或方法
  • 闪电:表示事件
    在这里插入图片描述
4、具体应用
  • 事件的使用一般通过发布者订阅者来进行。发布者会在某一条件下触发某事件,订阅者可以通过订阅该事件,来对该事件的触发做出反应。其类似于广播---->>接收的模型。
  • 一个约定俗称的规定是:订阅事件的方法(即响应函数)命名,通常为On + 事件名
using System;

namespace Pr01_Basic
{
    class Program
    {
        static void Main(string[] args)
        {
            Publisher pub = new Publisher();
            Subscriber sub = new Subscriber();

            // 1、通过事件发布器来触发事件,然后订阅器接收该事件
            pub.NumChange += new NumCountChange(sub.OnNumChange);
            pub.InvokeEvent(); //发布者触发事件,订阅者接收该触发事件
            
            // 2、通过委托则可以在外部直接触发该事件,为可行但不合理的方式
            pub.NumChangeDelegate = new NumCountChange(sub.OnNumChange);
            pub.NumChangeDelegate(200);//委托变量直接调用了订阅者的OnNumChange函数   

            Console.ReadKey();
        }  
    }

    /// <summary>
    /// 声明委托类型
    /// </summary>
    /// <param name="num"></param>
    public delegate void NumCountChange(int num);


    /// <summary>
    /// 事件发布者类
    /// </summary>
    class Publisher
    {
        public NumCountChange NumChangeDelegate; //声明委托类型变量

        public event NumCountChange NumChange; //声明事件

        public int count { set; get; } = 99;

        //当调用该函数时,由发布者来触发对应的事件
        public void InvokeEvent()
        {
            if(NumChange != null) //当委托中有函数注册时(即为非空),就会触发该事件
            {
                ++count;
                NumChange(count);//触发该事件
            }
        }

    }

    /// <summary>
    /// 事件订阅者类
    /// </summary>
    class Subscriber
    {
        //该函数会注册到委托事件中,一旦事件发布者触发该事件时,订阅者就会接收到该事件进行执行
        public void OnNumChange(int count)
        {
            Console.WriteLine("Subscriber the changed number is {0}", count);
        }

    }

}

主函数中通过事件公共类型委托这两种方式进行对比:

  1. 通过事件的方式:发布者触发事件,订阅者接收事件

    • 首先将订阅者Subscriber的响应函数OnNumChange注册到事件NumChange上,这样就可以通过事件来触发对应的响应函数。
    • 由于事件在类外部除了注册或注销函数外,不能被访问,所以事件只能由发布者内部来进行触发。在此是通过发布者的实例pub调用InvokeEvent函数,进而在发布者内部触发事件,而不能直接在外部进行调用,如:pub.NumChange(99)是不允许的。这样就保证了类的封装性。
  2. 通过公共类型委托变量的方式:

    • 首先将订阅者Subscriber的响应函数OnNumChange注册到委托NumChangeDelegate上。由于委托变量为公有的,所以它可以在类外部被直接访问。
    • 在这里可以直接通过调用委托变量:pub.NumChangeDelegate(200),来触发刚刚注册到其上的响应函数OnNumChange。该方式可行,但是其在外部触发事件,破坏了类的封装性。
    • 该方式同时也可以调用触发事件函数pub.InvokeEvent(),来在发布者内部实现触发事件。
  3. 以上两种方式,由于公共类型委托的方式有可能在类外部直接触发事件,而事件的方式则只允许发布者触发事件,订阅者接收事件。因此,事件的方式会更加的合理,也实现了对类更好的封装性。以上实例运行结果如下:
    在这里插入图片描述


二、事件中的观察者模式

1、事件触发模式
  • 事件的触发和反应是一种观察者模式,主要由两部分组成:
    • 被监视主体:相当于事件发布者,其中有一些属性来被其它对象所监视。
    • 观察者:相当于事件的订阅者,它会观察被监视对象的某些属性,当这些属性发生变化时,会触发观察者做出对应的反应。观察者一般可以有多个。
  • 被监视对象只管触发事件,而不管是否有人订阅了该事件。
  • 观察者只根据自身的需要,来决定是否需要注册订阅该事件,以对该事件产生响应,而不管是谁触发了该事件。因此,观察者模式是一种松耦合的设计模式
2、具体实例

加热水的加热器,时刻显示水温的显示器,当水温达到90度时的报警器。这三者来共同组成该程序,代码如下:

using System;
using System.Threading;

namespace Pr02_Delegate
{
    class Program
    {
        static void Main(string[] args)
        {
            Heater heater = new Heater();
            Alarm alarm = new Alarm();
            Display display = new Display();

            heater.curTemperature += alarm.MakeAlarm; //将报警函数注册到事件上
            heater.curTemperature += display.ShowTemperature; //将显示温度函数注册到事件上

            heater.Heating();//开始加热

        }
    }

    public delegate void SendTemperatureEvent(int num);

    /// <summary>
    /// 加热器:负责加热。相当于事件发布者,用来发布当前水温。
    /// </summary>
    class Heater
    {
        public event SendTemperatureEvent curTemperature;

        private int temperature;
        private int maxTemp = 100;

        public void Heating()
        {
        //水温从85度开始
            for(int i=85;i<=maxTemp;++i)
            {
                temperature = i;

                if(curTemperature!=null)
                {
                    curTemperature(temperature);
                }
                
                Thread.Sleep(100);
            }

            Console.WriteLine("Now the temperatue is up to 100 °C !!!");
            Console.ReadKey();
        }


    }

    /// <summary>
    /// 报警器:当水温达到一定的值时,进行报警
    /// </summary>
    class Alarm
    {
        //订阅者:从加热器那里订阅水温,当水温达到一定值,就开始报警。
        public void MakeAlarm(int temperature)
        {
            if(temperature>90)
            {
                Console.WriteLine("The temperature is up to {0}", temperature);
            }
        }
    }

    /// <summary>
    /// 显示器:显示水温值
    /// </summary>
    class Display
    {
        //订阅者:从加热器那里订阅水温,并进行显示。
        public void ShowTemperature(int temperature)
        {
            Console.WriteLine("The temperature = {0}", temperature);
        }
    }
}

  • 加热器在此为被监视主体,它的属性水温temperature是被监视的对象;报警器和显示器为两个观察者,来观察加热器的水温这一属性,当水温变化达到某一条件时就会触发对应的事件。
  • 将观察者中的函数注册到事件中,然后开始调用主体中的函数Heating。该函数将会触发类内部的事件。由于该事件已经注册了报警和显示等函数,因此就会触发对应的响应函数。
  • 事件的委托在调用前,必须要判断其是否有注册函数,否则当事件中未注册函数而被调用时,就会产生无对象可调用的异常。
  • 运行结果如下图所示:
    在这里插入图片描述

参考内容:C# 中的委托和事件(详解)


三、常见事件示例

1、定时器事件Timer
  • 要包含对应的命名空间:using System.Timers;
  • 指定时间间隔,然后每到指定的间隔就响应一次,代码如下:
namespace Pr01_Basic
{
    class Program
    {
        static void Main(string[] args)
        {
            Test te = new Test();

            Timer timer = new Timer();
            timer.Interval = 1000;
            //C#中事件和行为是通过 += 来进行连接。
            //timer.Elapsed表示时间到,进行触发
            //te.Response为对该触发事件做出的响应
            timer.Elapsed += te.Response;
            timer.Start();

            Console.ReadKey();
        }  
    }
    class Test
    {       
        //事件的响应函数
        internal void Response(object sender, ElapsedEventArgs e)
        {
            Console.WriteLine("Hello is a response for Timer!");
        }
    }
}

2、事件触发和响应分别为不同对象

在这里插入图片描述

通过Form类来创建对象,并实现点击对应窗口框,显示字符串。

  • 首先要在References上添加对应的Form引用,操作如下所示:
    在这里插入图片描述
  • 然后在代码中添加引用:using System.Windows.Forms;

代码如下:

namespace Pr01_Basic
{
    class Program
    {
        static void Main(string[] args)
        {
            Form fm = new Form(); //事件触发对象
            Test te = new Test(fm); //事件响应对象
            fm.ShowDialog();

            Console.ReadKey();
        }  
    }
    class Test
    {
        public Form form { get; set; }

        public Test(Form form)
        {
            if(form!=null)
            {
                this.form = form;
                this.form.Click += this.formClick; //点击时触发事件与响应
            }
        }

        private void formClick(object sender, EventArgs e)
        {
            this.form.Text = "Just!!";
        }
    }
}

运行结果如下:
在这里插入图片描述

3、事件的触发和响应都是同一个对象

点击窗口,其自身做出响应

namespace Pr01_Basic
{
    class Program
    {
        static void Main(string[] args)
        {
            MyForm myfm = new MyForm();
            //事件的触发者和响应者皆为myfm
            myfm.Click += myfm.Response;
            myfm.ShowDialog();

            Console.ReadKey();
        }  
    }

    class MyForm : Form
    {
        internal void Response(object sender, EventArgs e)
        {
            this.Text = "Test My Form";
        }
    }
}

四、自定义事件

  • 21
    点赞
  • 117
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值