什么是事件?
需要了解:
- 事件的概念
- 事件的应用
- 理解事件与委托
- 事件的声明
- 问题辨析(事件是特殊的委托吗?)
事件的概念
- 定义:Event,译为“事件”
- 角色:使对象或类具备通知能力的成员
- 中译:事件是一种使对象或类能够提供通知的成员。
- 英译:An event is a member that enables an object or class to provide notifications.
- “对象O拥有一个事件E” 想表达的意思是,当事件E发生的时候,O有能力通知其他的对象。
- 当手机响了的时候,在手机的角度来看,它是在通知关注他的人,要求关注它的人采取行动,而在人的角度来看,人收到了通知,可以采取行动了。更进一步的说
- 伴随着通知,很多通知在发生的时候会伴随着很多信息的出现,以我们的手机为例,如果手机响了,可能是你收到了微信,或者是工作通知,或者是app推送。站在手机的角度来看,它是在通知关注者的同时,将相关的消息也发送给关注者了。而站在关注者的角度来看,他除了被通知到了之外,还收到了事件的主体(手机)经由事件发送过来的消息。而在C#中,这种消息被称为 “事件参数”(EventArgs),被通知者根据参数里的内容,来采取相应的行动,如果是会议体系就准备参加会议,如果是电话则根据电话号码是接听还是不接听,而我们根据通知和事件参数来采取行动的这种行为成为响应事件or处理事件,处理事情时具体所做的事情被称为 “事件处理器”(EventHandler)
- 不是所有的事件,都带有信息,例如大楼的火警预报响了,在它发起通知后,你没有进行判断就已经跑路了,此类型的事件,当发生时就说明了一切。所以事件 = 通知 +(可选)的参数。
- 当手机响了的时候,在手机的角度来看,它是在通知关注他的人,要求关注它的人采取行动,而在人的角度来看,人收到了通知,可以采取行动了。更进一步的说
- 使用:用于对象或类之间的动作协调与消息传递(消息推送)
- 现在当别人告诉你这个类或对象中有一个事件,说明这个类在发送通知了之后,关注它的订阅者们也就会根据事件的类型,事件的信息,纷纷做出动作协调。
- 原理:事件模型(event model)中的两个5
- “发生--响应”中的五个部分 —— 闹钟响了你起床,孩子饿了你做饭....这里面还隐含了“订阅关系”。你只关心你的闹钟和你的孩子,这便是订阅。
- “发生--响应”中的五个步骤 —— (1)我有一个事件 -- (2)一个人或者一群人关心我的这个事件 -- (3) 我的这个事件发生了 --(4) 关心这个事件的人依次被通知到 --(5)被通知到的人根据拿到我的事件信息(又称“事件数据”,“事件参数”,“通知”)对事件进行响应(又称处理事件)。
事件的应用:
事件模型的五个组成部分:
- 事件的拥有者(event source,对象)
- 事件的成员 (event,成员)
- 事件的响应者 (event subscriber,对象)
- 事件的处理器 (event handler,成员)—— 本质是一个回调方法
- 事件订阅 ——把事件处理器与事件关联到一起,本质上是一种以委托类型为基础的 “约定”
- 约定是指在C#中,用于订阅事件的事件处理器,必须和事件遵守同一个约定,这个约定即表明了事件能够把什么样的消息发送给事件处理器,也约束了事件处理器能够处理什么样的消息。当遵守了这个约定之后,我们便可以认为事件和事件处理器是匹配的。则可以用事件处理器去订阅事件了。否则不匹配,不能够订阅。
事件模型的应用组合方式:
1. 事件的拥有者和事件的响应者分别是两个不同的对象,且都包含者他们的事件成员
using System; using System.Timers; namespace LanguageLearn { class Program { static void Main(string[] args) { //事件的拥有者 var timer = new Timer(); timer.Interval = 1000; //事件的响应者 var boy = new Boy(); var girl = new Girl(); //订阅 timer.Elapsed += boy.Action; timer.Elapsed += girl.Action; timer.Start(); Console.ReadLine(); } } class Boy { //事件的处理器 internal void Action(object sender, ElapsedEventArgs e) { Console.WriteLine("Jump!"); } } class Girl { internal void Action(object sender, ElapsedEventArgs e) { Console.WriteLine("Sing!"); } } }
2. 事件方法订阅了本身的方法
using System; using System.Windows.Forms; namespace LanguageLearn4._8 { 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) { _form = form; _form.Click += this.FormClicked; } } private void FormClicked(object sender, EventArgs e) { _form.Text = DateTime.Now.ToString(); } } }
3. 事件的响应者是事件的拥有者的一部分,比如一个大菜单栏通知了事件,而事件的响应者是里面的小菜单
using System; using System.Windows.Forms; namespace LanguageLearn4._8 { internal class Program { static void Main(string[] args) { //事件的拥有者 也是事件的响应者 MyForm form= new MyForm(); //事件的源 以及事件的订阅 form.Click += form.FormClick; form.ShowDialog(); } } class MyForm : Form { //事件的处理程序 internal void FormClick(object sender, EventArgs e) { this.Text = DateTime.Now.ToString(); } } }
4. 事件的拥有者是事件的响应者的一部分,这种模式最常见,如我们的应用程序和按钮的关系。
using System; using System.Windows.Forms; namespace LanguageLearn4._8 { internal class Program { static void Main(string[] args) { MyForm form = new MyForm(); form.ShowDialog(); } } //事件的拥有者为事件的响应者的一部分 class MyForm : Form { private TextBox _textBox; private Button _button; public MyForm() { this._textBox = new TextBox(); this._button = new Button(); this.Controls.Add(this._textBox); this.Controls.Add(this._button); this._button.Text = "Click"; //事件 this._button.Click += this.ButtonClicked; this._button.Top = 100; } //事件处理器 private void ButtonClicked(object sender, EventArgs e) { this._textBox.Text = DateTime.Now.ToString(); } } }
理解事件与委托:
- 事件是基于委托的:
- 事件需要用于委托类型来做一个约束,约束规定了事件能发送什么样的消息给响应者,也规定了响应者能收到什么样的消息。
- 事件响应的事件处理器必须与约束所匹配。
- 当事件的响应者向事件的拥有者提供了能够匹配这个事件的事件处理器之后呢,需要把事件处理器保存和记录下来。能够记录和引用方法也只有委托类型的实例能够做到。
综上所述,无论是表层的约束,还是底层的实现,都是需要依赖于委托类型的。
- 有了委托字段/属性,为什么还需要事件?
为了让程序的逻辑更加“有道理”、更加安全,防止“借刀杀人”。有了委托作为条件限制的事件,他在对象外部只允许 += or -= 也即是订阅事件和取消订阅事件,而在方法内部才允许发起事件的流程,这为我们提供了一定的保护性,不允许一个对象发起另一个对象的事件,导致程序的混乱,这就是为什么安全的道理。
小例子:
using System; using System.Threading; namespace LanguageLearn { class Program { static void Main(string[] args) { Customer customer = new Customer(); Waiter waiter = new Waiter(); //事件订阅 customer.Order += waiter.Action; // 顾客没有进行点菜 //customer.Action(); //不安全的对象 恶意的让customer进行委托点菜 Customer badCustomer = new Customer(); //这时候委托对象是一个字段 可以调用Invoke方法。 OrderEventArgs orderEventArgs = new OrderEventArgs() { DishName = "帮你点的菜", Size = "big" }; badCustomer.Order += waiter.Action; badCustomer.Order.Invoke(customer, orderEventArgs); //用户需要给钱 离谱哇 customer.PayBill(); } } //用于传递事件消息 public class OrderEventArgs : EventArgs { public string DishName { get; set; } public string Size { get; set; } } //用来处理Event 使用EventHandler作为后缀 //委托用来约束事件处理器 在此处消息约束对象是顾客 带有的信息为订单消息 public delegate void OrderEventHandler(Customer customer, OrderEventArgs e); //事件的拥有者 public class Customer { //错误的事件声明 声明成了委托 public OrderEventHandler Order; public double Bill { get; set; } public void PayBill() { Console.WriteLine($"I will pay ${Bill}"); } public void WalkIn() { Console.WriteLine("Walk into the restaurant"); } public void SitDown() { Console.WriteLine("Sit on a Desk"); } public void Think() { for (int i = 0; i < 5; i++) { Console.WriteLine("Let me think... "); Thread.Sleep(1000); } //若没有事件处理器来响应我们的事件 则不应该触发事件 if (this.Order != null) { //发起事件 this.Order.Invoke(this, new OrderEventArgs { DishName = "Kongpao Chicken", Size = "big" }); } } 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 Server the Dish - {e.DishName}"); double price = 10; switch (e.Size) { case "small": price *= 0.5; break; case "big": price *= 1.5; break; default: break; } customer.Bill += price; } } }
输出:
- 所以事件的本质是委托字段的一个包装器
- 这个包装器对委托字段的访问起限制作用,是一层“蒙板”。
- 在OO中封装的一个重要功能,就是隐藏。
- 事件对外界隐藏了委托实例的大部分功能,仅暴露添加/移除事件处理器的功能。
- 事件与委托的关系
- 事件真的是 “以特殊方式声明的委托字段/实例” 吗?
- 不是!只是声明的时候看起来像(对比委托字段与事件的简化声明,filed-like)。但是你反编译之后还是可以看见事件的完整生命。
- 事件声明的时候使用了委托类型,简化声明造成事件看上去像一个委托字段,而event关键字更像是一个修饰符。——这是错觉来源之一。
- 订阅事件的时候 += 操作符后面可以是一个委托实例,这与委托实例的赋值方法相同,这也让事件看起来更像是一个委托字段。——这是错觉来源之一。
- 重申:事件的本质是加装在委托字段上的一个“蒙板”(Mask),是起一个掩蔽作用的包装器,这个用于抵御非法操作的“蒙板”绝不是委托字段本身。
- 事件真的是 “以特殊方式声明的委托字段/实例” 吗?
- 为什么要使用委托类型来声明事件?
- 站在sourse的角度讲,是为了表明source能传递出哪些信息。
- 站在subscriber的角度讲,它是一种约定,它约定了你应该用什么样的签名的方法来处理(响应)事件。
- 委托类型的实例将用于存储(引用)事件处理器。
- 类比事件和属性
- 属性不是字段——很多时候属性是字段的包装器,这个包装器保证字段不被乱用。有时候是字段的逻辑判断器,保证字段是一个合法的值。
- 事件不是委托字段——他是委托字段的包装器,这个包装器用来保护委托字段不被滥用
- 包装器永远都不可能是被包装的东西。
事件的声明:
- 完整声明:
下面一个例子是 模拟顾客进饭店点菜吃完结账的一个事件流程。
using System; using System.Threading; namespace LanguageLearn { class Program { static void Main(string[] args) { Customer customer = new Customer(); Waiter waiter = new Waiter(); //事件订阅 customer.Order += waiter.Action; customer.Action(); customer.PayBill(); } } //用于传递事件消息 public class OrderEventArgs : EventArgs { public string DishName { get; set; } public string Size { get; set; } } //用来处理Event 使用EventHandler作为后缀 //委托用来约束事件处理器 在此处消息约束对象是顾客 带有的信息为订单消息 public delegate void OrderEventHandler(Customer customer, OrderEventArgs e); //事件的拥有者 public class Customer { //事件的完整性委托约束 private OrderEventHandler orderEventHandler; public event OrderEventHandler Order { add { this.orderEventHandler += value; } remove { this.orderEventHandler -= value; } } public double Bill { get; set; } public void PayBill() { Console.WriteLine($"I will pay ${Bill}"); } public void WalkIn() { Console.WriteLine("Walk into the restaurant"); } public void SitDown() { Console.WriteLine("Sit on a Desk"); } public void Think() { for (int i = 0; i < 5; i++) { Console.WriteLine("Let me think... "); Thread.Sleep(1000); } //若没有事件处理器来响应我们的事件 则不应该触发事件 if (this.orderEventHandler != null) { //发起事件 this.orderEventHandler.Invoke(this, new OrderEventArgs { DishName = "Kongpao Chicken", Size = "big" }); } } 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 Server the Dish - {e.DishName}"); double price = 10; switch (e.Size) { case "small": price *= 0.5; break; case "big": price *= 1.5; break; default: break; } customer.Bill += price; } } }
- 简略声明(字段式的声明 field-like):
using System; using System.Threading; namespace LanguageLearn { class Program { static void Main(string[] args) { Customer customer = new Customer(); Waiter waiter = new Waiter(); //事件订阅 customer.Order += waiter.Action; customer.Action(); customer.PayBill(); } } //用于传递事件消息 public class OrderEventArgs : EventArgs { public string DishName { get; set; } public string Size { get; set; } } //用来处理Event 使用EventHandler作为后缀 //委托用来约束事件处理器 在此处消息约束对象是顾客 带有的信息为订单消息 public delegate void OrderEventHandler(Customer customer, OrderEventArgs e); //事件的拥有者 public class Customer { //事件的简略式声明 public event OrderEventHandler Order; public double Bill { get; set; } public void PayBill() { Console.WriteLine($"I will pay ${Bill}"); } public void WalkIn() { Console.WriteLine("Walk into the restaurant"); } public void SitDown() { Console.WriteLine("Sit on a Desk"); } public void Think() { for (int i = 0; i < 5; i++) { Console.WriteLine("Let me think... "); Thread.Sleep(1000); } //若没有事件处理器来响应我们的事件 则不应该触发事件 if (this.Order != null) { //发起事件 this.Order.Invoke(this, new OrderEventArgs { DishName = "Kongpao Chicken", Size = "big" }); } } 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 Server the Dish - {e.DishName}"); double price = 10; switch (e.Size) { case "small": price *= 0.5; break; case "big": price *= 1.5; break; default: break; } customer.Bill += price; } } }
- 用于声明事件的委托类型的命名约定
- 用于声明Foo的事件的委托,一般命名为FooEventHandler。
- FooEventHandler委托的参数一般有两个
- 第一个是object类型的,名字为sender,实际上就是事件的拥有者,事件的source。
- 第二个是EventArgs类的派生类,类名一般为FooEventArgs,参数名为e,也就是事件参数,事件发生时带来的信息。
- 我们可以把委托的参数列表看作是事件发生后发送给事件响应者的“事件消息”。
- 触发Foo事件的方法一般命名为OnFoo,即“事出有因”
- 访问级别为protected,不能为public,不然又可以“借刀杀人”
- 事件的命名约定
- 带有时态的动词或动词短语
- 事件拥有者“正在做”什么事情,使用进行时;事件拥有者“做完了什么事情”,用完成时。