事件详解
在C#中,**事件(events)**是一种基于委托的机制,用于实现发布-订阅模式。
初步了解事件
事件的功能 = 通知 + 可选的事件参数(即详细信息)
各种程序架构就是为了避免使用事件模式而导致一团乱麻的最佳解决方案。
就是MVC、MVP、MVVM等模式
事件的应用
示例
简单示例
这个示例还表示,一个事件可以挂接多个事件处理器。
using System.Timers;
using Timer = System.Timers.Timer;
namespace EventExample {
internal class Program {
static void Main(string[] args) {
//作为事件的拥有者
Timer timer = new System.Timers.Timer();
//设置时间间隔
timer.Interval = 1000;
//事件的响应者
Boy boy = new Boy();
Girl girl = new Girl();
/* 对事件进行订阅
* += 的左边是被订阅的事件
* += 的右边是事件处理器
*
* 一开始不知道怎么写事件处理器
* 可以直接写boy.Action,然后将光标移动到有红色波浪线的地方
* 再按下Alt + Enter
* 编译器会自动生成事件处理器
*/
//Elapsed是每隔一段时间就会触发的
timer.Elapsed += boy.Action;
timer.Elapsed += girl.Action;
//当Elapsed事件发生的时候,boy的Action方法就会响应
timer.Start();
//读取输入用于暂停演示
Console.ReadLine();
}
}
//事件的响应者
class Boy {
//系统生成的这个事件处理器中的Object参数是一个可空的类型
internal void Action(object? sender, ElapsedEventArgs e) {
Console.WriteLine("Jump!");
}
}
class Girl {
internal void Action(object? sender, ElapsedEventArgs e) {
Console.WriteLine("Sing!");
}
}
}
一星示例
一星的组合方式,是很多设计模式(例如:MVC等)的雏形
using System;
using System.Windows.Forms;
namespace OneStart {
internal class Program {
static void Main(string[] args) {
//作为事件的拥有者
Form form = new Form();
//事件的响应者
Controller controller = new Controller(form);
//显式窗口
form.ShowDialog();
}
}
class Controller {
private Form form;
public Controller(Form form)
{
if(form != null) {
this.form = form;
//给Click事件添加事件处理器,即事件被订阅
this.form.Click += this.FormClicked;
}
}
//注意这个事件处理器的参数和之前的timer.Elapsed的事件处理器的参数是不一样的
//说明事件和事件处理器需要有所匹配,就是依靠事件处理器的参数
private void FormClicked(object sender, EventArgs e) {
this.form.Text = DateTime.Now.ToString();
}
}
}
两星示例
using System;
using System.Windows.Forms;
namespace OneStart {
internal class Program {
static void Main(string[] args) {
//既是事件的拥有者,又是事件的响应者
MyForm form = new MyForm();
//给Click事件添加事件触发器
form.Click += form.FormClicked;
form.ShowDialog();
}
}
class MyForm : Form {
//事件触发器
internal void FormClicked(object sender, EventArgs e) {
this.Text = DateTime.Now.ToString();
}
}
}
三星示例
事件的拥有者,是事件的响应者的一个字段成员,
然后事件的响应者用自己的方法订阅了自己的成员的某个事件
这个模式是最常用到的
using System;
using System.Windows.Forms;
namespace OneStart {
internal class Program {
static void Main(string[] args) {
//事件的响应者
MyForm myForm = new MyForm();
myForm.ShowDialog();
}
}
/* 现在我们想要做一个窗口
* 窗口中有一个文本框和一个按钮
* 按下按钮,文本框就会显示Hello,World!
* 所以我们需要继承多个类
*/
class MyForm : Form {
private TextBox textbox;
//事件的拥有者
private Button button;
public MyForm() {
this.textbox = new TextBox();
this.button = new Button();
//将控件显示在窗口中
this.Controls.Add(this.button);
this.Controls.Add(this.textbox);
//我们需要控制文本框显示文字,是在检测到按钮被按下的时候
//点击事件发生的时候,就会响应ButtonClicked方法
this.button.Click += this.ButtonClicked;
this.button.Text = "Say Hello";
this.button.Top = 30;
//这就是不可视化编程
}
//事件处理器
private void ButtonClicked(object sender, EventArgs e) {
this.textbox.Text = "Hello, World!!!!!!";
}
}
}
事件小知识
一个事件处理器,是可以被重用的,重用的前提条件就是,和被处理的事件的约束一致,也就是匹配。
在这个基础上,我们可以根据事件的来源不同,从而给出不同的响应
namespace OtherExample {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
//sender表示事件的发生者,即事件的来源
//两个button的点击事件都绑定的同一个事件处理器
private void ButtonClicked(object sender, EventArgs e) {
if(sender == this.button1 ) {
this.textBox1.Text = "这是第一个button发送的";
}
if(sender == this.button2 ) {
this.textBox1.Text = "这是第二个button发送的";
}
}
}
}
一个事件可以挂接多个事件处理器,一个事件处理器也可以被多个事件所挂接。
前半句的示例,就是前面第一个示例。
后半句的示例就在下面:
namespace OtherExample {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
//第一种挂接事件的方式:
this.button3.Click += this.ButtonClicked;
//第二种挂接事件的方式:
//EventHandler是一个委托
// 这是传统的挂接方式
this.button3.Click += new EventHandler(this.ButtonClicked);
//第三种,匿名挂接
this.button3.Click += delegate (Object sender, EventArgs e) {
this.textBox1.Text = "Mr.Okay";
};
//匿名挂接,Lambda表达式形式
//this.button3.Click += (Object sender, EventArgs e) => {
// this.textBox1.Text = "Mr.Okay";
//};
甚至可以省略,参数的类型,只写参数名
this.button3.Click += (sender, e) => {
this.textBox1.Text = "Mr.Okay";
};
}
//sender表示事件的发生者,即事件的来源
private void ButtonClicked(object sender, EventArgs e) {
if(sender == this.button1 ) {
this.textBox1.Text = "这是第一个button发送的";
}
if(sender == this.button2 ) {
this.textBox1.Text = "这是第二个button发送的";
}
if(sender == this.button3 ) {
this.textBox1.Text = "Mr.Okay";
}
}
}
}
WPF中的挂接方式和WForm中的事件挂接方法是相似的
事件的声明
之前我们已经学了如何使用事件,现在我们需要学习如何定义事件,即如何写事件的拥有者,这部分。
简略声明和委托的声明很近似,所以我们最好先学习如何完整的声明一个事件,再学简略的声明,
避免和委托的声明弄混。
如果觉得一个事件很难写,就按照之前学的那五个组成部分去写,准没错!
完整的声明
事件是基于委托的,这句话有两层含义
事件需要使用委托类型来做一个约束
这个约束,既规定了,事件可以发送什么样的消息给事件的,又规定了,事件的响应者能收到什么样的消息
这就决定了事件响应者的事件处理器必须和这个事件匹配上,才能订阅这个事件当事件的响应者向事件的拥有者提供了能够匹配这个事件的事件处理器之后
简单来说就是,匹配上了之后, 还需要保存和记录事件处理器,而能够记录或者引用方法的任务也只有委托类型的实例才能做到, 所以这个地方就应用到了委托总结来说:事件这种成员,无论从表层约束来讲,还是从底层实现来讲,都是依赖于委托类型的
namespace EventExample {
//模拟服务员订阅客人点菜
internal class Program {
static void Main(string[] args) {
//事件拥有者
Customer customer = new Customer();
//事件的响应者
Waiter waiter = new Waiter();
customer.Order += waiter.Action;
customer.Think();
customer.PayTheBill();
}
}
//存储信息的数据类型
//定义的类名需要后缀,为了增加可读性
//并且这种类必须继承EventArgs
// 注意以下三个类的访问级别必须保持一致,因为以后是要一起使用的,
//访问级别不一致就会产生冲突
public class OrderEventArgs : EventArgs
{
public string? DishName { get; set; }
public string? Size { get; set; }
}
/* 因为没有合适的委托类型供我们来声明我们需要的事件
* 所以我们需要自己声明一个委托类型
* 规定事件处理器的类型为void
* 第一个参数是事件的来源
* 第二个参数是事件附带的信息
* 当下是点菜的事件,那么附带的信息可能就有菜名和分量等
* 但是我们没有能够存储这些信息的数据类型,
* 所以还要去声明这个数据类型
*/
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
//事件的拥有者
public class Customer {
//1. 声明一个委托类型的字段
// 就是用来引用事件处理器的
private OrderEventHandler? orderEventHandler;
/* 2. 声明事件
* 想要被外界访问到,修饰符就必须是public
* event关键字
* 拿哪个委托类型来约束这个事件
* 最后是事件名字
*/
public event OrderEventHandler Order {
//首先是事件处理器的添加器
//代码的意思就是,添加事件处理器
add {
this.orderEventHandler += value;
//value在这里的作用就像是,外部添加事件的模版
}
//事件处理器的移除器
//代码的意思就是,移除事件处理器
remove {
this.orderEventHandler -= value;
}
}
public double Bill { get; set; }
public void PayTheBill() {
Console.WriteLine($"I will pay ${this.Bill}");
}
//触发事件的方法
public void Think() {
for (int i = 0; i < 5; i++) {
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
//当为空,也就是没有人订阅这个事件,就会抛异常
if (this.orderEventHandler != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "Kongpao Chicken";
e.Size = "large";
//启动委托
//事件是基于委托来控制的,感觉就像是委托的一种使用
this.orderEventHandler.Invoke(this, e);
}
}
}
//事件的响应者
class Waiter {
//这个就是事件处理器
//通过前面事件的定义,
//我们也就知道,事件处理器的参数列表为什么是这样子的了
internal void Action(Customer customer, OrderEventArgs e) {
Console.WriteLine($"I will serve you the dish - {e.DishName}");
double price = 10;
switch (e.Size) {
case "small":
price = price * 0.5;
break;
case "large":
price = price * 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
完整声明的事件,只能放在 +=
和 -=
的左边。
简略声明
类似于字段的声明方法
相对于事件的完整声明,事件的简略声明中,我们并没有手动去声明用于存储引用事件处理器的委托字段,但是通过反编译可以发现,这个字段仍然会有,编译器,会自动帮我们生成
namespace EventExample {
//模拟服务员订阅客人点菜
internal class Program {
static void Main(string[] args) {
//事件拥有者
Customer customer = new Customer();
//事件的响应者
Waiter waiter = new Waiter();
customer.Order += waiter.Action;
customer.Think();
customer.PayTheBill();
}
}
//定义的类名需要后缀,为了增加可读性
//并且这种类必须继承EventArgs
// 注意以下三个类的访问级别必须保持一致,因为以后是要一起使用的,
//访问级别不一致就会产生冲突
public class OrderEventArgs : EventArgs
{
public string? DishName { get; set; }
public string? Size { get; set; }
}
/* 因为没有合适的委托类型供我们来声明我们需要的事件
* 所以我们需要自己声明一个委托类型
* 规定事件处理器的类型为void
* 第一个参数是事件的来源
* 第二个参数是事件附带的信息
* 当下是点菜的事件,那么附带的信息可能就有菜名和分量等
* 但是我们没有能够存储这些信息的数据类型,
* 所以还要去声明这个数据类型
*/
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
//事件的拥有者
public class Customer {
1. 声明一个委托类型的字段
就是用来引用事件处理器的
//private OrderEventHandler? orderEventHandler;
///* 2. 声明事件
// * 想要被外界访问到,修饰符就必须是public
// * event关键字
// * 拿哪个委托类型来约束这个事件
// * 最后是事件名字
// */
//public event OrderEventHandler Order {
// //首先是事件处理器的添加器
// //代码的意思就是,添加事件处理器
// add {
// this.orderEventHandler += value;
// }
// //事件处理器的移除器
// //代码的意思就是,移除事件处理器
// remove {
// this.orderEventHandler -= value;
// }
//}
/* 事件的简略声明,用于代替上面的两个步骤
* public event [作为事件的约束以及存储对事件处理器的引用的委托类型] [事件名]
* 这种类似于,字段声明的方法,但并不真的是字段
* 我们实际上并没有声明一个委托字段,那么对事件处理器的引用在哪里?
* 实际上,那个委托类型字段还是在的,编译器帮我们声明了
*/
public event OrderEventHandler Order;
public double Bill { get; set; }
public void PayTheBill() {
Console.WriteLine($"I will pay ${this.Bill}");
}
//触发事件的方法
public void Think() {
for (int i = 0; i < 5; i++) {
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
//当为空,也就是没有人订阅这个事件,就会抛异常
if (this.Order != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "Kongpao Chicken";
e.Size = "large";
this.Order.Invoke(this, e);
/* 当省略声明的时候,也就不存在orderEventHandler了
* 所以,orderEventHandler也就不能使用了
* 要直接使用Order,来代替orderEventHandler
*/
}
}
}
//事件的响应者
class Waiter {
//这个就是事件处理器
//通过前面事件的定义,
//我们也就知道,事件处理器为什么是这样子的了
internal void Action(Customer customer, OrderEventArgs e) {
Console.WriteLine($"I will serve you the dish - {e.DishName}");
double price = 10;
switch (e.Size) {
case "small":
price = price * 0.5;
break;
case "large":
price = price * 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
为什么需要事件
事件的使用实际上是对委托在语言安全性上的控制,委托就是C++中的指针,而指针的滥用往往会导致很多的问题,虽然委托,是在指针的基础上的,而且加强了安全性,但是仍然会出现滥用之类的情况,所以,事件也是为避免滥用。
这就是为什么Java中彻底放弃了与函数指针相关的功能
不使用事件,直接使用委托,那么委托,可能会被其他东西直接使用,而事件可以在语言上避免这种问题,也就是,之前例子中的Order
事件只能在+=
或-=
操作符的左边出现,不能直接使用点操作符使用
不过有个小小的问题,为什么在Customer
类的内部可以直接使用事件呢?
- 在
Customer
类内部能够使用Order事件去做非空比较以及调用Order.Invoke
方法纯属于不得已而为之,因为使用事件的简化声明时,我们没有手动声明一个委托类型的字段。虽然编译器会自动生成那个委托类型字段,但是我们是访问不到的,所以只能使用事件的名字来做这件事情。这是微软编译器语法糖所造成的语法冲突和前后不一致
安全性泄露的案例:
namespace EventExample {
//模拟服务员订阅客人点菜
internal class Program {
static void Main(string[] args) {
//事件拥有者
Customer customer = new Customer();
//事件的响应者
Waiter waiter = new Waiter();
customer.Order += waiter.Action;
//customer.Think();
//当我们不使用这个方法时,理论上不应该有别人来帮我们点菜
//但是不使用事件,而使用委托类型的字段
//那么就可能发生,在这个Customer类之外的随便访问这个委托字段
OrderEventArgs e = new OrderEventArgs();
e.DishName = "Manhanquanxi";
e.Size = "large";
OrderEventArgs e2 = new OrderEventArgs();
e2.DishName = "Beer";
e2.Size = "large";
//借刀杀人
Customer badGuy = new Customer();
badGuy.Order += waiter.Action;
//将点的菜记在customer
badGuy.Order.Invoke(customer, e);
badGuy.Order.Invoke(customer, e2);
/* 自己写程序的时候,可以注意点,不出现这种不安全的调用的问题
* 但是,多人协作的时候,这种问题,如果没有在语言层面来限制
* 那么这种自由度,就很有可能会被误用、滥用
*/
customer.PayTheBill();
}
}
//定义的类名需要后缀,为了增加可读性
//并且这种类必须继承EventArgs
// 注意以下三个类的访问级别必须保持一致,因为以后是要一起使用的,
//访问级别不一致就会产生冲突
public class OrderEventArgs : EventArgs
{
public string? DishName { get; set; }
public string? Size { get; set; }
}
/* 因为没有合适的委托类型供我们来声明我们需要的事件
* 所以我们需要自己声明一个委托类型
* 规定事件处理器的类型为void
* 第一个参数是事件的来源
* 第二个参数是事件附带的信息
* 当下是点菜的事件,那么附带的信息可能就有菜名和分量等
* 但是我们没有能够存储这些信息的数据类型,
* 所以还要去声明这个数据类型
*/
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
//事件的拥有者
public class Customer {
//直接声明字段
//不使用事件
//程序也可以完全运行,并且和使用事件的运行结果一样
//但是安全性已经泄露了
public OrderEventHandler Order;
public double Bill { get; set; }
public void PayTheBill() {
Console.WriteLine($"I will pay ${this.Bill}");
}
//触发事件的方法
public void Think() {
for (int i = 0; i < 5; i++) {
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
//当为空,也就是没有人订阅这个事件,就会抛异常
if (this.Order != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "Kongpao Chicken";
e.Size = "large";
this.Order.Invoke(this, e);
/* 当省略声明的时候,也就不存在orderEventHandler了
* 所以,orderEventHandler也就不能使用了
* 要直接使用Order,来代替orderEventHandler
*/
}
}
}
//事件的响应者
class Waiter {
//这个就是事件处理器
//通过前面事件的定义,
//我们也就知道,事件处理器为什么是这样子的了
internal void Action(Customer customer, OrderEventArgs e) {
Console.WriteLine($"I will serve you the dish - {e.DishName}");
double price = 10;
switch (e.Size) {
case "small":
price = price * 0.5;
break;
case "large":
price = price * 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
-
事件的本质是委托字段的一个包装器
- 这个包装器对委托字段的访问起限制作用,相当于一个“蒙板”
- 封装的一个重要的功能就是隐藏
- 事件对外界隐藏了委托实例的大部分功能,仅暴露添加/移除事件处理器的功能
- 事件的触发必须由事件的拥有者自己来做
事件的委托类型命名约定
使用简略的事件声明,并且省略委托的定义,直接使用自带的EventHandler
委托
namespace EventExample {
//模拟服务员订阅客人点菜
internal class Program {
static void Main(string[] args) {
//事件拥有者
Customer customer = new Customer();
//事件的响应者
Waiter waiter = new Waiter();
customer.Order += waiter.Action;
customer.Think();
customer.PayTheBill();
}
}
//定义的类名需要后缀,为了增加可读性
//并且这种类必须继承EventArgs
// 注意以下三个类的访问级别必须保持一致,因为以后是要一起使用的,
//访问级别不一致就会产生冲突
public class OrderEventArgs : EventArgs
{
public string? DishName { get; set; }
public string? Size { get; set; }
}
/* 因为没有合适的委托类型供我们来声明我们需要的事件
* 所以我们需要自己声明一个委托类型
* 规定事件处理器的类型为void
* 第一个参数是事件的来源
* 第二个参数是事件附带的信息
* 当下是点菜的事件,那么附带的信息可能就有菜名和分量等
* 但是我们没有能够存储这些信息的数据类型,
* 所以还要去声明这个数据类型
*/
//public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
//还可以省略委托的声明,使用自带的EventHandler委托
//事件的拥有者
public class Customer {
//直接声明字段
//不使用事件
//程序也可以完全运行,并且和使用事件的运行结果一样
//但是安全性已经泄露了
public event EventHandler Order;
public double Bill { get; set; }
public void PayTheBill() {
Console.WriteLine($"I will pay ${this.Bill}");
}
//触发事件的方法
public void Think() {
for (int i = 0; i < 5; i++) {
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
//当为空,也就是没有人订阅这个事件,就会抛异常
if (this.Order != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "Kongpao Chicken";
e.Size = "large";
this.Order.Invoke(this, e);
/* 当省略声明的时候,也就不存在orderEventHandler了
* 所以,orderEventHandler也就不能使用了
* 要直接使用Order,来代替orderEventHandler
*/
}
}
}
//事件的响应者
class Waiter {
//通过自带的EventHandler委托来定义的事件处理器
internal void Action(Object sender, EventArgs e) {
Customer customer = sender as Customer;
OrderEventArgs orderInfo = e as OrderEventArgs;
Console.WriteLine($"I will serve you the dish - {orderInfo.DishName}");
double price = 10;
switch (orderInfo.Size) {
case "small":
price = price * 0.5;
break;
case "large":
price = price * 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
正确命名触发事件的方法
namespace EventExample {
//模拟服务员订阅客人点菜
internal class Program {
static void Main(string[] args) {
//事件拥有者
Customer customer = new Customer();
//事件的响应者
Waiter waiter = new Waiter();
customer.Order += waiter.Action;
customer.Think();
customer.PayTheBill();
}
}
//定义的类名需要后缀,为了增加可读性
//并且这种类必须继承EventArgs
// 注意以下三个类的访问级别必须保持一致,因为以后是要一起使用的,
//访问级别不一致就会产生冲突
public class OrderEventArgs : EventArgs
{
public string? DishName { get; set; }
public string? Size { get; set; }
}
/* 因为没有合适的委托类型供我们来声明我们需要的事件
* 所以我们需要自己声明一个委托类型
* 规定事件处理器的类型为void
* 第一个参数是事件的来源
* 第二个参数是事件附带的信息
* 当下是点菜的事件,那么附带的信息可能就有菜名和分量等
* 但是我们没有能够存储这些信息的数据类型,
* 所以还要去声明这个数据类型
*/
//public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
//还可以省略委托的声明,使用自带的EventHandler委托
//事件的拥有者
public class Customer {
//直接声明字段
//不使用事件
//程序也可以完全运行,并且和使用事件的运行结果一样
//但是安全性已经泄露了
public event EventHandler Order;
public double Bill { get; set; }
public void PayTheBill() {
Console.WriteLine($"I will pay ${this.Bill}");
}
/* 在面对对象编程中
* 一个方法最好只做一件事情
*/
public void Think() {
for (int i = 0; i < 5; i++) {
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
this.OnOrder("Kongpao Chicken", "large");
//其实,这里的逻辑,没有完善起来
//但是仅用于演示
}
//按照标准来写,触发事件的方法
protected void OnOrder(string dishName, string size) {
//一定要判断所封装的委托是否为空
//当为空,也就是没有人订阅这个事件,就会抛异常
if (this.Order != null) {
OrderEventArgs e = new OrderEventArgs();
e.DishName = dishName;
e.Size = size;
this.Order.Invoke(this, e);
/* 当省略声明的时候,也就不存在orderEventHandler了
* 所以,orderEventHandler也就不能使用了
* 要直接使用Order,来代替orderEventHandler
*/
}
}
}
//事件的响应者
class Waiter {
//通过自带的EventHandler委托来定义的事件处理器
internal void Action(Object sender, EventArgs e) {
Customer customer = sender as Customer;
OrderEventArgs orderInfo = e as OrderEventArgs;
Console.WriteLine($"I will serve you the dish - {orderInfo.DishName}");
double price = 10;
switch (orderInfo.Size) {
case "small":
price = price * 0.5;
break;
case "large":
price = price * 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
注:
一个类对外最重要的接口,就是它的三类成员:属性、方法、事件
对应三个功能,存储数据,做事情,通知别人
事件与委托的关系
为什么事件是基于委托的?
- 站在事件来源的角度来看,是为了表明来源能够对外传递哪些消息
- 站在事件的订阅者的角度来看,它是一种约定,是为了约束能够使用什么样签名的方法来处理(响应)事件
- 委托类型的实例将用于存储(引用)事件处理器
记住:事件保护的是委托字段
总结来说,事件这篇内容难度挺大的。