# 事件(Event)
事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件。例如,中断。
C# 中使用事件机制实现线程间的通信。
提示:
- 事件多用于桌面、手机等开发的客户端编程,因为这些程序经常是用户通过事件来 “驱动” 的
- 各种编程语言对这个机制的实现方法不尽相同
- Java 语言里没有事件这种成员,也没有委托这种数据类型。Java 的 “事件” 是使用接口来实现的
- MVC、MVP、MVVM 等模式,是事件模式更高级、更有效的 “玩法”
- 日常开发的时候,使用已有事件的机会比较多,自己声明事件的机会比较少,所以一定要做到熟练使用
# 事件的应用
事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。
事件的五个组成部分
- 事件的拥有者(event source,对象或类)
只有事件的拥有者才能触发事件 - 事件成员(event,成员)
事件自己不会主动的去通知事件的响应者,只有在事件的拥有者的某些内部逻辑触发以后,他才会去通知事件的响应者。 - 事件的响应者(event subscriber,对象)
订阅了事件的对象或者类 - 事件处理器(eventHandler,成员)
本质上是一个回调方法,它属于事件的响应者的内部方法 - 事件订阅
把事件处理器(一个函数 / 方法)与事件关联在一起,本质上是一种以委托类型为基础的约定(所以事件是基于委托的)
# 事件的使用
事件的拥有者和事件的响应者之间存在三种情况
- 事件的拥有者和事件的响应者是两个不同的对象
- 事件的拥有者和事件的响应者是同一个对象
- 事件的拥有者是事件的响应者的字段成员(事件的响应者用自己的方法订阅字段成员的事件)
注意
- 事件处理器是方法成员
- 挂接事件处理器的时候,可以使用委托实例,也可以直接使用方法名
- 事件处理器对事件的订阅不是随意的,匹配与否由声明事件时所使用的委托类型来检测
- 事件可以同步调用也可以异步调用
事件的拥有者和事件的响应者是两个不同的对象
public class EventTest | |
{ | |
private static void Main(string[] args) | |
{ | |
//Form 是一个窗体 | |
Form form = new Form(); // 事件的拥有者 | |
Controller controller = new Controller(form); // 事件的响应者 | |
form.ShowDialog(); | |
} | |
} | |
public class Controller | |
{ | |
private Form CForm; | |
public Controller(Form form) | |
{ | |
if (form != null) | |
{ | |
CForm = form; | |
//Click 是事件 | |
CForm.Click += FormClicked; // 订阅事件 | |
} | |
} | |
/// <summary> | |
/// 事件处理器 | |
/// 第二个参数是事件的约定,响应事件的前提是处在同一种约定下 | |
///sender 是:事件消息的发送者 | |
/// EventArgs 是:用来传递事件信息的,也就是事件的约束 | |
/// </summary> | |
/// <param name="sender"> 事件消息的发送者 & lt;/param> | |
/// <param name="e"> 事件信息 & lt;/param> | |
/// <exception cref="NotImplementedException"></exception> | |
private void FormClicked(object sender, EventArgs e) | |
{ | |
CForm.Text = DateTime.Now.ToString(); | |
} | |
} |
事件的拥有者和事件的响应者是同一个对象
public class EventTest | |
{ | |
private static void Main(string[] args) | |
{ | |
MyForm myForm = new MyForm(); | |
myForm.Click += myForm.MyFormClicked; | |
myForm.ShowDialog(); | |
} | |
} | |
public class MyForm : Form | |
{ | |
/// <summary> | |
/// 事件处理器 | |
/// </summary> | |
/// <param name="sender"></param> | |
/// <param name="e"></param> | |
internal void MyFormClicked(object sender, EventArgs e) | |
{ | |
this.Text = DateTime.Now.ToString(); | |
} | |
} |
事件的拥有者是事件的响应者的字段成员
public class EventTest | |
{ | |
private static void Main(string[] args) | |
{ | |
MyForm myForm = new MyForm(); // 事件的响应者 | |
myForm.ShowDialog(); | |
} | |
} | |
public class MyForm : Form | |
{ | |
private TextBox MyTextBox; | |
private Button MyButton; // 事件的拥有者,是一个字段成员 | |
public MyForm() | |
{ | |
MyTextBox = new TextBox(); | |
MyButton = new Button(); | |
Controls.Add(MyButton); | |
Controls.Add(MyTextBox); | |
MyButton.Text = "MyButton"; | |
MyTextBox.Top = 25; | |
//Click 是事件 | |
MyButton.Click += MyButtonClicked; // 订阅事件 | |
} | |
/// <summary> | |
/// 事件处理器 | |
/// </summary> | |
/// <param name="sender"></param> | |
/// <param name="e"></param> | |
/// <exception cref="NotImplementedException"></exception> | |
private void MyButtonClicked(object sender, EventArgs e) | |
{ | |
MyTextBox.Text = DateTime.Now.ToString(); | |
} | |
} |
# 事件信息类
- 命名规定:如果类是用来传递事件信息的,需要在名字后面加上 EventArgs(事件参数)
- 继承规定:必须继承 EventArgs,这是微软准备好的基类
public class MyEventArgs : EventArgs | |
{ | |
} |
# 用于事件的委托
- 命名规定:如果委托是为了声明某个事件的,需要在名字后面加上 EventHandler(事件处理程序)
- EventHandler 的意义:这个委托是用来 声明事件、约束事件处理器,创建的实例是用来储存事件处理器的
- 一般情况下,事件信息的参数名是 e
- 一般情况下,使用微软做好的委托就行了(EventHandler)
public delegate void MyEventHandler(object sender, MyEventArgs e); |
# 事件的声明
完整的声明方式
// 声明委托 | |
// 保护级别必须是 private | |
private MyEventHandler myEventHandler; | |
// 声明事件 | |
public event MyEventHandler Order | |
{ | |
add { myEventHandler += value; } | |
remove { myEventHandler -= value; } | |
} |
简略的声明方式
// 反编译后可以得知:系统会自动生成委托类型的字段并将其隐藏,内部逻辑与完整的声明方式相同 | |
public event MyEventHandler Order; |
使用微软做好的委托来声明事件
public event EventHandler Order; |
# 触发事件
// 定义委托 | |
public delegate void OrderEventHandler(object sender, EventArgs e); | |
public class Test | |
{ | |
// 声明委托 | |
private OrderEventHandler orderEventHandler; | |
// 声明事件 | |
public event OrderEventHandler Order | |
{ | |
add { orderEventHandler += value; } | |
remove { orderEventHandler -= value; } | |
} | |
// 声明事件 | |
public event EventHandler Order; | |
/// <summary> | |
/// 触发事件的方法 | |
/// 保护级别是 protected 或 private | |
/// </summary> | |
/// <param name="e"></param> | |
private void OnOrder(EventArgs e) | |
{ | |
/* 完整的触发方式 */ | |
// 判断是否有事件处理器(判断储存事件处理器的委托是否为空) | |
if (this.orderEventHandler != null) | |
{ | |
// 只有事件的拥有者才能触发事件,所以第一个参数是 this | |
orderEventHandler.Invoke(this, e); // 触发事件 | |
} | |
/* 简略的触发方式 */ | |
this.orderEventHandler?.Invoke(this, e); | |
/* 使用事件触发 */ | |
// 事件只能用 += 和 -= | |
// 使用简略的声明方式时,委托类型的字段被隐藏了, | |
// 所以这里的做法是 不得已而为之 | |
// 这就导致了语法的冲突,是微软的问题 | |
if (Order != null) | |
{ | |
// 只有事件的拥有者才能触发事件,所以一个参数是 this | |
Order.Invoke(this, e); // 触发事件 | |
} | |
/* 事件的简略触发方式 */ | |
this.Order?.Invoke(this, e); | |
} | |
} |
# 一个完整的事件处理流程
接下来,我们模拟一个情况:顾客点餐
- 顾客(事件的拥有者)
- 顾客点餐(事件)
- 服务员(事件的响应者)
- 服务员等待顾客发起点餐(事件订阅)
- 服务员处理顾客的点餐(事件处理器)
namespace EventsAndDelegates | |
{ | |
public class EventTest | |
{ | |
private static void Main(string[] args) | |
{ | |
Customer customer_0 = new Customer(); // 事件的拥有者 | |
Waiter waiter_0 = new Waiter(); // 事件的响应者 | |
OrderEventArgs e = new OrderEventArgs() | |
{ | |
FoodName = "XXXX", | |
Size = OrderEventArgs.AllSize.Large, | |
}; | |
//Order 是事件 | |
customer_0.Order += waiter_0.WaiterAction; // 订阅事件 | |
customer_0.Action(); | |
string temp = Console.ReadLine(); | |
while (temp == "1" || temp == "2") | |
{ | |
switch (temp) | |
{ | |
case "1": | |
customer_0.CustomerOrder(e); | |
break; | |
case "2": | |
customer_0.PayMoney(); | |
break; | |
} | |
temp = Console.ReadLine(); | |
} | |
Console.ReadLine(); | |
} | |
} | |
/// <summary> | |
/// 传递事件信息 | |
/// </summary> | |
public class OrderEventArgs : EventArgs | |
{ | |
public enum AllSize | |
{ | |
Small, | |
Medium, | |
Large, | |
} | |
public string FoodName { get; set; } | |
public AllSize Size { get; set; } | |
} | |
/// <summary> | |
/// 事件的拥有者 | |
/// </summary> | |
public class Customer | |
{ | |
// 使用微软做好的委托来声明事件 | |
public event EventHandler Order; | |
public float Money { get; set; } // 客人需要付的钱 | |
/// <summary> | |
/// 触发事件的方法 | |
/// </summary> | |
/// <param name="e"></param> | |
protected void OnOrder(OrderEventArgs e) | |
{ | |
if (Order != null) | |
{ | |
Order.Invoke(this, e); // 触发事件 | |
} | |
} | |
public void PayMoney() | |
{ | |
Console.WriteLine("I will pay: " + Money); | |
} | |
private void WalkIn() | |
{ | |
Console.WriteLine("WalkIn"); | |
} | |
private void SetDown() | |
{ | |
Console.WriteLine("SetDown"); | |
} | |
private void Think() | |
{ | |
Console.WriteLine("Let me think..."); | |
Thread.Sleep(1000); | |
} | |
public void Action() | |
{ | |
WalkIn(); | |
SetDown(); | |
} | |
public void CustomerOrder(OrderEventArgs e) | |
{ | |
Think(); | |
OnOrder(e); | |
} | |
} | |
/// <summary> | |
/// 事件的响应者 | |
/// </summary> | |
public class Waiter | |
{ | |
/// <summary> | |
/// 事件处理器 | |
/// </summary> | |
/// <param name="customer"> 事件消息的发送者 & lt;/param> | |
/// <param name="e"> 事件信息 & lt;/param> | |
/// <exception cref="NotImplementedException"></exception> | |
public void WaiterAction(object sender /*Customer customer*/, EventArgs e) | |
{ | |
Customer customer = sender as Customer; | |
OrderEventArgs orderEventArgs = e as OrderEventArgs; | |
float price = 20; | |
switch (orderEventArgs.Size) | |
{ | |
case OrderEventArgs.AllSize.Small: | |
price *= 0.5f; | |
break; | |
case OrderEventArgs.AllSize.Medium: | |
break; | |
case OrderEventArgs.AllSize.Large: | |
price *= 1.5f; | |
break; | |
} | |
customer.Money += price; | |
Console.WriteLine("Food Name: " + orderEventArgs.FoodName + "\tPrice: " + price); | |
} | |
} | |
} |
# 事件与委托的关系
- 事件真的是 “以特殊方式声明的委托字段 / 实例” 吗?
- 不是!只是声明的时候 “看起来像”(对比委托字段与事件的简化声明,field-like)
- 事件声明的时候使用了委托类型,简化声明造成事件看上去像一个委托的字段 (实例),而 event 关键字则更像是一个修饰符 —— 这就是错觉的来源之一
- 订阅事件的时候 += 操作符后面可以是一个委托实例,这与委托实例的赋值方法语法相同,这也让事件看起来像是一个委托字段 —— 这是错觉的又一来源
- 重申:事件的本质是加装在委托字段上的一个 “蒙板”(mask),是一个起掩蔽作用的包装器。这个用于阻挡非法操作的 “蒙板” 绝不是委托字段本身
- 为什么要使用委托类型来声明事件?
- 站在 source 的角度来看,是为了表明 source 能对外传递哪些消息
- 站在 subscriber 的角度来看,它是一种约定,是为了约束能够使用什么样签名的方法来处理 (响应) 事件
- 委托类型的实例将用于存储 (引用) 事件处理器
- 对比事件与属性
- 属性不是字段 —— 很多时候,属性是字段的包装器,这个包装器用来保护字段不被滥用
- 事件不是委托字段 —— 它是委托字段的包装器,这个包装器用来保护委托字段不被滥用
- 包装器永远都不可能是被包装的东西