一、事件基础
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);
}
}
}
主函数中通过事件和公共类型委托这两种方式进行对比:
-
通过事件的方式:发布者触发事件,订阅者接收事件
- 首先将订阅者
Subscriber
的响应函数OnNumChange
注册到事件NumChange
上,这样就可以通过事件来触发对应的响应函数。 - 由于事件在类外部除了注册或注销函数外,不能被访问,所以事件只能由发布者内部来进行触发。在此是通过发布者的实例
pub
调用InvokeEvent
函数,进而在发布者内部触发事件,而不能直接在外部进行调用,如:pub.NumChange(99)
是不允许的。这样就保证了类的封装性。
- 首先将订阅者
-
通过公共类型委托变量的方式:
- 首先将订阅者
Subscriber
的响应函数OnNumChange
注册到委托NumChangeDelegate
上。由于委托变量为公有的,所以它可以在类外部被直接访问。 - 在这里可以直接通过调用委托变量:
pub.NumChangeDelegate(200)
,来触发刚刚注册到其上的响应函数OnNumChange
。该方式可行,但是其在外部触发事件,破坏了类的封装性。 - 该方式同时也可以调用触发事件函数
pub.InvokeEvent()
,来在发布者内部实现触发事件。
- 首先将订阅者
-
以上两种方式,由于公共类型委托的方式有可能在类外部直接触发事件,而事件的方式则只允许发布者触发事件,订阅者接收事件。因此,事件的方式会更加的合理,也实现了对类更好的封装性。以上实例运行结果如下:
二、事件中的观察者模式
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";
}
}
}