初步了解事件
- 定义:单词Event,译为”事件”
- 《牛津词典》中的解释是”a thing that happens,especially something important”
- 通顺的解释就是”能够发生的什么事情”
- 角色:使对象或类具备通知能力的成员
- (中译)事件(event)是一种使对象或类能够提供通知的成员
- (原文)An event is a member that enables an object or class to provide notifications
- “对象O拥有一个事件E”想表达的思想是:当事件E发生的时候,O有能力通知别的对象
- 使用:用于对象或类间的动作协调与信息传递(消息推送)
- 原理:事件模型(event model)中的两个”5”
- “发生→响应”中的5个部分——闹钟响了你起床、孩子饿了你做饭…这里隐含着”订阅”关系
- “发生→响应”中的5个动作——(1)我有一个事件→(2)一个人或者一群人关心我的这个事件→(3)我的这个事件发生了→(4)关心这个事件的人会被依次通知到→(5)被通知到的人根据拿到的事件信息(又称”事件数据”、”事件参数”、”通知”)对事件进行响应(又称”处理事件”)
- 提示
- 事件多用于桌面、手机等开发的客户端编程,因为这些程序经常是用户通过事件来”驱动”的
- 各种编程语言对这个机制的实现方法不尽相同
- Java语言里没有事件这种成员,也没有委托这种数据类型。Java的”事件”是使用接口来实现的
- MVC、MVP、MVVM等模式,是事件模式更高级、更有效的”玩法”
- 日常开发的时候,使用已有事件的机会比较多,自己声明事件的机会比较少,所以先学使用
事件的应用
事件模型的五个组成部分
- 事件的拥有者(event source,对象)
- 事件成员(event,成员)
- 事件的响应者(event subscriber,对象)
- 事件处理器(event handler,成员)——本质上是一个回调方法
- 事件订阅——把事件处理器与事件关联在一起,本质上是一种以委托类型为基础的”约定”
- 示例:一个事件同时有两个事件处理器订阅 ```csharp using System; using System.Timers;
namespace EventExample { class Program { static void Main(string[] args) { Timer timer = new Timer(); timer.Interval = 1000; Boy boy = new Boy();
//事件拥有者:timer
//事件:Elapsed
//事件响应者:boy对象
//事件处理器是DoSomething方法
//事件订阅:"timer.Elapsed += boy.DoSomething;",事件订阅的操作符:+=
timer.Elapsed += boy.DoSomething;
Girl girl = new Girl();
timer.Elapsed += girl.Action;
timer.Start();
Console.ReadLine();
}
}
class Boy
{
//此方法可以通过在"timer.Elapsed += boy.DoSomething;"Alt+Enter自动补齐
internal void DoSomething(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Jump!");
}
}
class Girl
{
//事件处理器:Action
internal void Action(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Sing!");
}
}
}
<a name="VLrT6"></a>
## 常见的组合方式
<a name="Yw0sd"></a>
### 事件拥有者和事件响应者是两个不同的对象
- 标准的事件机制模型,MVC、MVP等设计模式的雏形
![image.png](https://cdn.nlark.com/yuque/0/2021/png/21507654/1625016958101-afb8d8fa-47ef-4172-9378-203f7c9aa134.png#clientId=ucb8214ea-9dfe-4&from=paste&height=102&id=ud6e3ebe7&margin=%5Bobject%20Object%5D&name=image.png&originHeight=203&originWidth=484&originalType=binary&ratio=2&size=58487&status=done&style=none&taskId=u608502be-07d1-42a4-b6c0-44d6cd696fd&width=242)
- 示例:
```csharp
using System;
using System.Windows.Forms;
namespace EventExample
{
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)
{
form = _form;
//事件拥有者:form
//事件:Click
//事件响应者:this,即外部创建的controller对象
//事件处理器:FormClicked
//事件订阅:"form.Click += this.FormClicked;"
form.Click += this.FormClicked;
}
}
private void FormClicked(object sender, EventArgs e)
{
form.Text = DateTime.Now.ToString();
}
}
}
事件的拥有者同时也是事件的响应者
- 对象用自己的方法订阅了自己的一个事件,较为常见
- 示例: ```csharp using System; using System.Windows.Forms;
namespace EventExample { class Program { static void Main(string[] args) { MyForm form = new MyForm();
//事件拥有者:form
//事件:Click
//事件响应者:form
//事件处理器:FormClicked
//事件订阅:"form.Click += form.FormClicked;"
form.Click += form.FormClicked;
form.ShowDialog();
}
}
class MyForm : Form
{
internal void FormClicked(object sender, EventArgs e)
{
this.Text = DateTime.Now.ToString();
}
}
}
<a name="KdENd"></a>
### 事件的拥有者是事件响应者的一个字段成员
- 事件的响应者用自己的方法订阅了自己的字段成员的某个时间
- 比如有一个窗口对象,当中有一个按钮,按钮是窗口的一个字段成员,按钮有一个click事件,按钮是事件的拥有者,当对窗口进行编程时,窗口中可能会有一个方法,用来订阅按钮的click事件。
- 应用广泛(Windows平台默认的事件订阅和处理结构)
![image.png](https://cdn.nlark.com/yuque/0/2021/png/21507654/1625017381977-ee2a49c5-094e-4e2f-9fe1-1342e58c4579.png#clientId=ucb8214ea-9dfe-4&from=paste&height=122&id=u31de119f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=244&originWidth=411&originalType=binary&ratio=2&size=66292&status=done&style=none&taskId=u575c3a10-1715-42ac-a5ca-26bbfb17d88&width=205.5)
- 示例:非可视化编程,编写程序时看不到界面的状态。使用winfrom程序,即可进行可视化编程,所见即所得
```csharp
using System;
using System.Windows.Forms;
namespace EventExample
{
class Program
{
static void Main(string[] args)
{
MyForm form = new MyForm();
form.ShowDialog();
}
}
class MyForm : Form
{
private TextBox textBox;
private Button button1;
private Button button2;
private Button button3;
private Button button4;
public MyForm()
{
this.textBox = new TextBox();
this.button1 = new Button();
this.button2 = new Button();
this.button3 = new Button();
this.button4 = new Button();
this.Controls.Add(this.textBox);
this.Controls.Add(this.button1);
this.Controls.Add(this.button2);
this.Controls.Add(this.button3);
this.Controls.Add(this.button4);
//事件拥有者:this.button1,是MyForm的一个字段成员
//事件:Click
//事件响应者:this,也就是MyForm创建的对象
//事件处理器:ButtonClicked
//事件订阅:"this.button.Click += this.ButtonClicked;"
this.button1.Click += this.ButtonClicked;
this.button2.Click += new EventHandler(this.ButtonClicked);//基于委托,WinForm中常用
this.button3.Click += delegate (object sender, EventArgs e) { this.textBox.Text = "Mr.Okay!"; };//匿名方法,已过时
this.button4.Click += (object sender, EventArgs e) => { this.textBox.Text = "Lambda Func!"; };//用lambda表达式对匿名方法进行改写,且object和EventArgs可省略
this.button1.Text = "button1";
this.button2.Text = "button2";
this.button3.Text = "button3";
this.button4.Text = "button4";
this.button1.Top = 50;
this.button2.Top = 100;
this.button3.Top = 150;
this.button4.Top = 200;
}
private void ButtonClicked(object sender, EventArgs e)
{
if (sender == this.button1)
{
this.textBox.Text = "Hello!";
}
if (sender == this.button2)
{
this.textBox.Text = "World!";
}
}
}
}
事件的声明
完整声明
- 示例: ```csharp using System; using System.Threading;
namespace EventExample { class Program { static void Main(string[] args) { Customer customer = new Customer(); Waiter waiter = new Waiter();
//事件拥有者:customer
//事件:Order
//事件响应者:waiter
//事件处理器:Action
//事件订阅:customer.Order += waiter.Action;
customer.Order += waiter.Action;
customer.Action();
customer.PayTheBill();
}
}
public class OrderEventArgs : EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}
//声明委托类型
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
public class Customer
{
//声明委托类型字段,用来存储事件处理器
private OrderEventHandler orderEventHandler;
//声明事件,OrderEventHandler表示拿此委托类型约束事件
public event OrderEventHandler Order
{
//事件处理器添加器
add
{
this.orderEventHandler += value;
}
//事件处理器移除器
remove
{
this.orderEventHandler -= value;
}
}
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine($"I will pay ${this.Bill}");
}
public void WalkIn()
{
Console.WriteLine("Walk into the restaurant.");
}
public void SitDown()
{
Console.WriteLine("Sit down.");
}
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()
{
Size = "large",
DishName = "Kongpao Chicken"
};
this.orderEventHandler.Invoke(this, e);
}
}
public void Action()
{
Console.ReadLine();
this.WalkIn();
this.SitDown();
this.Think();
}
}
public class Waiter
{
public 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;
}
}
}
<a name="mpr4b"></a>
## 简略声明(字段式声明,file-like)
- 示例:将上例改成事件简略声明,只需将Customer类改写成如下:
```csharp
public class Customer
{
//声明事件,简略申明
public event OrderEventHandler Order;
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine($"I will pay ${this.Bill}");
}
public void WalkIn()
{
Console.WriteLine("Walk into the restaurant.");
}
public void SitDown()
{
Console.WriteLine("Sit down.");
}
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
//事件触发,必须由事件拥有者来做
//此处用Order事件名代替以前的orderEventHandle委托类型字段,是一个语法糖
if (this.Order != null)
{
OrderEventArgs e = new OrderEventArgs()
{
Size = "large",
DishName = "Kongpao Chicken"
};
this.Order.Invoke(this, e);
}
}
public void Action() { ...}
}
有了委托字段/属性,为何还需要事件?
- 为了程序的逻辑更加”有道理”、更加安全。谨防”借刀杀人”
- 示例:“借刀杀人” ```csharp using System; using System.Threading;
namespace EventExample { class Program { static void Main(string[] args) { Console.ReadLine(); Customer customer = new Customer(); Waiter waiter = new Waiter(); customer.Order += waiter.Action; //customer.Action();
OrderEventArgs e = new OrderEventArgs()
{
DishName = "Manhanquanxi",
Size = "large"
};
OrderEventArgs e2 = new OrderEventArgs()
{
DishName = "Beer",
Size = "large"
};
Customer badGuy = new Customer();
//此时Order委托类型字段已被滥用,可以Invoke给第一个customer
//而事件不能".Invoke",事件只能"+="操作,从而约束了外部的滥用
badGuy.Order += waiter.Action;
badGuy.Order.Invoke(customer, e);
badGuy.Order.Invoke(customer, e2);
//customer付了badGuy点餐的费用
customer.PayTheBill();
}
}
public class OrderEventArgs : EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}
//声明委托类型
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
public class Customer
{
//Order变成了委托类型字段
public OrderEventHandler Order;
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine($"I will pay ${this.Bill}");
}
public void WalkIn()
{
Console.WriteLine("Walk into the restaurant.");
}
public void SitDown()
{
Console.WriteLine("Sit down.");
}
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
//事件触发,必须由事件拥有者来做
//此处用Order事件名代替以前的orderEventHandle委托类型字段,是一个语法糖
if (this.Order !=null)
{
OrderEventArgs e = new OrderEventArgs()
{
Size = "large",
DishName = "Kongpao Chicken"
};
this.Order.Invoke(this, e);
}
}
public void Action()
{
Console.ReadLine();
this.WalkIn();
this.SitDown();
this.Think();
}
}
public class Waiter
{
public 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;
}
}
}
<a name="QjlL7"></a>
# 事件的本质
- 事件的本质是委托字段的一个包装器
- 这个包装器对委托字段的访问起限制作用,相当于一个"蒙板"
- 封装(encapsulation)的一个重要功能就是隐藏
- 事件对外界隐藏了委托实例的大部分功能,仅暴露添加/移除事件处理器的功能
<a name="HcyiJ"></a>
# 用于声明事件的委托类型的命名约定
- 用于声明Foo事件的委托,一般命名为FooEventHandle(除非是一个非常通用的事件约束)
- FooEventHandle委托的参数一般有两个(由Win32 API演化而来,历史悠久)
- 第一个是object类型,名字为sender,实际就是事件的拥有者、事件的source
- 第二个是EventArgs类的派生类,类名一般为FooEventArgs,参数名为e。也就是前面讲过的事件参数
- 虽然没有官方的说法,但我们可以把委托的参数列表看做是事件发生后发送给事件响应者的"事件消息"
- 触发Foo事件的方法一般命名为OnFoo,即"因何引发"、"事出有因"
- 访问级别为protected,不能为public,不然又成了可以"借刀杀人"了
<a name="u2Fze"></a>
# 事件的命名约定
- 带有时态的动词或者动词短语
- 事件拥有者"正在做"什么事情,用进行时;事件拥有者"做完了"什么事情,用完成时
- 示例:对上例进行改写:
```csharp
using System;
using System.Threading;
namespace EventExample
{
class Program
{
static void Main(string[] args)
{
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += waiter.Action;
customer.Action();
customer.PayTheBill();
}
}
public class OrderEventArgs : EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}
public class Customer
{
//声明通用的事件约束EventHandler(C#内部的,而非自定义的委托类型)
public event EventHandler Order;
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine($"I will pay ${this.Bill}");
}
public void WalkIn()
{
Console.WriteLine("Walk into the restaurant.");
}
public void SitDown()
{
Console.WriteLine("Sit down.");
}
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)
{
//事件触发,必须由事件拥有者来做
//此处用Order事件名代替以前的orderEventHandle委托类型字段,是一个语法糖
//Order封装委托类型的调用方法
if (this.Order != null)
{
OrderEventArgs e = new OrderEventArgs()
{
Size = size,
DishName = dishName
};
this.Order.Invoke(this, e);
}
}
public void Action()
{
Console.ReadLine();
this.WalkIn();
this.SitDown();
this.Think();
}
}
public class Waiter
{
public 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 *= 0.5;
break;
case "large":
price *= 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
事件与委托的关系
- 事件真的是”以特殊方式声明的委托字段/实例”吗?
- 不是!只是声明的时候”看起来像”(对比委托字段与事件的简化声明,filed-like)
- 事件声明的时候使用了委托类型,简化声明造成事件看上去像一个委托的字段(实例),而event关键字则更像是一个修饰符——这就是错觉的来源之一
- 订阅事件的时候+=操作符后面可以是一个委托实例,这与委托实例的赋值方法语法相同,这也让事件看起来像是一个委托字段——这是错觉的又一来源
- 重申:事件的本质是加装在委托字段上的一个”蒙板”(mask),是个起掩蔽作用的包装器。这个用于阻挡非法操作的”蒙板”绝不是委托字段本身
- 为什么要使用委托类型来声明事件?
- 站在source的角度来看,是为了表明source能对外传递哪些消息
- 站在subscriber的角度来看,它是一种约定,是为了约束能够使用什么样签名的方法来处理(响应)事件
- 委托类的实例将用于存储(引用)事件处理器
- 对比事件与属性
- 属性不是字段——很多时候属性是字段的包装器,这个包装器用来保护字段不被滥用
- 事件不是委托字段——它是委托字段的包装器,这个包装器用来保护委托字段不被滥用
- 包装器永远都不可能是被包装的东西