每写一个事件例子,就找出来事件模型的五个组成部分,加深对事件模型的掌握。

初步了解事件

:::info

  • 定义:单词Event, 译为”事件”
    • 《牛津词典》中的解释是”a thing that happens, especially something important”
    • 通顺的解释就是”能够发生的什么事情”
  • 角色:使对象或类具备通知能力的成员

    • (中译) 事件(event) 是一种使对象或类能够提供通知的成员
    • (原文) An event is a member that enables an objector class to provide notifications.
    • “对象O拥有一个事件E“想表达的思想是:当事件E发生的时候,O有能力通知别的对象 ::: 以手机有响铃事件举列:
  • 手机可以通过响铃事件来通知关注手机的人

  • 响铃事件让手机具备了通知关注者的能力
  • 从手机角度看:
    • 响铃要求关注者采取行动
    • 通知关注者的同时,把相关消息也发送给关注者
  • 从人的角度看:
    • 人得到手机的通知,可以采取行动了
    • 除了得到通知,还收到了事件主体者(手机)经由事件发送过来的消息 事件参数 EventArgs
  • 响应事件:关注者得到通知后,检查事件参数,依据其内容采取响应的行动

    • 处理事件具体所做的事情:事件处理器 Event Handler
    • 如果是会议提醒:就去准备会议
    • 如果是电话接入:选择是否接听
    • 如果关注者在开会,直接抛弃掉事件参数,不做处理 :::info
  • 使用:用于对象或类间的动作协调与信息传递(消息推送) ::: 事件的功能 = 通知 + 可选的事件参数(即详细信息) :::info

  • 原理:事件模型(event model) 中的两个“5”

    • “发生→响应”中的5个部分——闹钟响了你起床、孩子饿了你做饭…….这里隐含着”订阅”关系

      1. ** 闹钟、响了、导致、你、起床**
    • “发生→响应”中的5个动作

      1. - (1)我有一个事件→
      2. - (2)一个人或者一群人关心我的这个事件→
      3. - (3)我的这个事件发生了→
      4. - (4)关心这个事件的人会被依次通知到→
      5. - (5)被通知到的人根据拿到的事件信息(又称"事件数据"、“事件参数“、“通知”)对事件进行响应(又称“处理事件”)。

      ::: :::info

  • 提示

    • 事件多用于桌面、手机等开发的客户端编程,因为这些程序经常是用户通过事件来“驱动”的
    • 各种编程语言对这个机制的实现方法不尽相同
    • Java语言里没有事件这种成员, 也没有委托这种数据类型。Java的“事件”是使用接口来实现的
    • MVC、MVP、MV VM等模式, 是事件模式更高级、更有效的“玩法”
    • 日常开发的时候,使用已有事件的机会比较多,自己声明事件的机会比较少,所以先学使用 :::

      术语约定

      下面五种说法都指事件的订阅者: :::tips 事件的订阅者
      事件消息的接收者
      事件的响应者
      事件的处理者
      被事件所通知的对象 ::: 下面四种说法都指事件参数: :::tips 事件信息
      事件消息
      事件数据
      事件参数 :::

      事件模式

      事件模式本身也是一种设计模式

事件模式有一些缺陷,例如牵扯到的元素比较多(5个),不加约束的话,程序逻辑很容易变得一团乱麻。

为了约束团队成员写代码时保持一致,把具有相同功能的代码写到固定的地方去,人们总结出一些最佳解决方案,逐渐形成了 MVC、MVP、MVVM 等程序架构模式。这些模式要求程序员在处理事件时有所为有所不为,代码该放到哪就放到哪,让程序更有条理。

事件的应用

实例演示

  • 派生(继承) 与扩展(extends)

事件模型的五个组成部分

  1. 事件的拥有者(event source, 对象)
  2. 事件成员(event, 成员)
  3. 事件的响应者(event subscriber,对象)
  4. 事件处理器(eventhandler,成员) ——本质上是一个回调方法
  5. 事件订阅——把事件处理器与事件关联在一起,本质上是一种以委托类型为基础的”约定”

注意

  • 事件处理器是方法成员
  • 挂接事件处理器的时候,可以使用委托实例,也可以直接使用方法名,这是个“语法糖”
  • 事件处理器对事件的订阅不是随意的,匹配与否由声明事件时所使用的委托类型来检测
  • 事件可以同步调用也可以异步调用

    事件拥有者通过内部逻辑触发事件

    用户按下按钮执行操作,看似是用户的外部操作引起按钮的 Click 事件触发,实际不然,详细情况大致如下:
  1. 当用户点击图形界面的按钮时,实际是用户的鼠标向计算机硬件发送了一个电信号。
  2. Windows 检测到该电信号后,就会检测鼠标当前在屏幕上的位置。当 Windows 发现鼠标位置处有个按钮,且包含该按钮的窗口处于激活状态,Windows就通知该按钮已被用户按下,然后按钮的内部逻辑开始执行。
  3. 典型的逻辑是按钮快速地把自己绘制一遍,绘制成自己被按下的样子,然后记录当前的状态为被按下。
  4. 紧接着如果用户松开了鼠标,Windows 就把消息传递给按钮,按钮内部逻辑又开始执行,把自己绘制成弹起的状态,记录当前的状态为未被按下。
  5. 按钮内部逻辑检测到,按钮被执行了连续的按下、松开动作,即按钮被点击了。按钮马上使用自己的 Click 事件通知外界,自己被点击了。如果有别的对象订阅了该按钮的 Click 事件,这些事件的订阅者就开始工作。

简言之:用户操作通过 Windows 调用了按钮的内部逻辑,最终还是按钮的内部逻辑触发了 Click 事件。

事件示例

Timer 的一些成员,其中闪电符号标识的两个就是事件:
2020122事件详解 - 图1
通过查看 Timer 的成员,我们不难发现一个对象最重要的三类成员:

  • 属性:对象或类当前处于什么状态
  • 方法:它能做什么
  • 事件:它能在什么情况下通知谁 ```csharp using System; using System.Timers; using Timer = System.Timers.Timer;

namespace EventExample { class Program { static void Main(string[] args) { // 1.事件拥有者 timer Timer timer = new Timer(); timer.Interval = 1000;//时间间隔长短,单位ms

  1. // 3.事件的响应者 boy
  2. Boy boy = new Boy();
  3. Girl girl = new Girl();
  4. // 2.事件成员 Elapsed,5.事件订阅 +=
  5. timer.Elapsed += boy.Action;
  6. timer.Elapsed += girl.Action;
  7. timer.Start();
  8. Console.ReadLine();
  9. }
  10. }
  11. class Boy
  12. {
  13. // 这是通过 VS 自动生成的事件处理器,适合新手上手。
  14. // 4.事件处理器 Action
  15. internal void Action(object sender, ElapsedEventArgs e)
  16. {
  17. Console.WriteLine("Jump!");
  18. }
  19. }
  20. class Girl
  21. {
  22. internal void Action(object sender, ElapsedEventArgs e)
  23. {
  24. Console.WriteLine("Sing!");
  25. }
  26. }

}

  1. <a name="JVcH2"></a>
  2. ## 几种事件订阅方式
  3. <a name="L3ONK"></a>
  4. ### ⭐事件拥有者和事件响应者是完全不同的两个对象
  5. ![](https://cdn.nlark.com/yuque/0/2018/png/101969/1539307270956-ddd7ed12-287a-4e76-90bb-dd4429c5a970.png#crop=0.0345&crop=0.0852&crop=0.8515&crop=0.9013&from=url&height=182&id=GIqZh&margin=%5Bobject%20Object%5D&originHeight=223&originWidth=579&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=473)<br />这种组合方式是 MVC、MVP 等设计模式的雏形。
  6. ```csharp
  7. using System;
  8. using System.Windows.Forms;
  9. namespace EventExample
  10. {
  11. class Program
  12. {
  13. static void Main(string[] args)
  14. {
  15. Form form = new Form();//1.事件拥有者
  16. Controller controller = new Controller(form);//3.事件响应者
  17. form.ShowDialog();
  18. }
  19. }
  20. class Controller
  21. {
  22. private Form form;
  23. public Controller(Form form)
  24. {
  25. if (form != null)
  26. {
  27. this.form = form;
  28. this.form.Click/*2.事件成员*/ += this.FormClicked;//5.事件订阅
  29. }
  30. }
  31. private void FormClicked(object sender, EventArgs e)//4.事件处理器
  32. {
  33. this.form.Text = DateTime.Now.ToString();
  34. }
  35. }
  36. }

Click 事件与上例的 Elapsed 事件的第二个参数的数据类型不同,即这两个事件的约定是不同的。
也就是说,你不能拿影响 Elapsed 事件的事件处理器去响应 Click 事件 —— 因为遵循的约束不同,所以他们是不通用的。

⭐⭐事件的拥有者和响应者是同一个对象

一个对象拿着自己的方法去订阅和处理自己的事件。
2020122事件详解 - 图2
该示例中事件的拥有者和响应者都是 from。示例中顺便演示了继承:

  1. using System;
  2. using System.Windows.Forms;
  3. namespace EventExample
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. // 1.事件拥有者 3.事件响应者 都是 from
  10. MyForm form = new MyForm();
  11. // 2.事件成员 Click 5.事件订阅 +=
  12. form.Click += form.FormClicked;
  13. form.ShowDialog();
  14. }
  15. }
  16. // 因为无法直接修改 Form 类,所以创建了继承与 Form 类的 MyForm 类
  17. class MyForm : Form/*后者为基类,也就是父类,前者为子类*///MyForm派生为Form的子类
  18. {
  19. // 4.事件处理器
  20. internal void FormClicked(object sender, EventArgs e)
  21. {
  22. this.Text = DateTime.Now.ToString();/*将标题栏的标题改为Datetime*/
  23. }
  24. }
  25. }

⭐⭐⭐事件的拥有者是事件响应者的一个字段成员

2020122事件详解 - 图3
Windows默认处理结构
这种是用得最广的:

  1. using System;
  2. using System.Windows.Forms;
  3. namespace EventExample
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. // 3.事件响应者 form
  10. MyForm form = new MyForm();
  11. form.ShowDialog();
  12. }
  13. }
  14. class MyForm : Form
  15. {
  16. private TextBox textBox;
  17. // 1.事件拥有者 button
  18. private Button button;
  19. public MyForm()//构造函数
  20. {
  21. this.textBox = new TextBox();
  22. this.button = new Button();
  23. this.Controls.Add(this.button);
  24. this.Controls.Add(this.textBox);
  25. // 2.事件成员 Click 5.事件订阅 +=
  26. this.button.Click += this.ButtonClicked;
  27. this.button.Text = "say hello";
  28. this.button.Top = 50;
  29. }
  30. /*此处编程为非可视化编程,所有操作结果都需要“run”之后才可显现*/
  31. // 4.事件处理器
  32. private void ButtonClicked(object sender, EventArgs e)
  33. {
  34. this.textBox.Text = "Hello, World!!!!";
  35. }
  36. }
  37. }

WinForm,WPF 事件绑定示例

WinForm 示例

2020122事件详解 - 图4

  1. public partical class Form1:Form
  2. {
  3. public Form1()
  4. {
  5. InitializeComponent();
  6. // 两种事件挂接的方式
  7. this.button3.Click += this.ButtonClicked;
  8. this.button3.Click += new EventHandler(this.ButtonClicked);
  9. //
  10. // 挂接匿名方法,别处无法进行引用操作
  11. // 已废弃的事件挂接方式
  12. this.button3.Click += delegate(object/*参数类型也可以不写*/ sender, EventArgs e)
  13. {
  14. this.textBox1.Text = "haha!";
  15. };
  16. // Lambda 表达式
  17. //PS:编译器可以通过委托约束推算出 sender 与 e 的数据类型
  18. this.button3.Click += (sender, e) =>
  19. {
  20. this.textBox1.Text = "Hoho!";
  21. };
  22. }
  23. private void ButtonClicked(object sender/*事件消息的发送者*/, EventArgs e)
  24. {
  25. if (sender == this.button1)
  26. {
  27. this.textBox1.Text = "Hello!";
  28. }
  29. if (sender == this.button2)
  30. {
  31. this.textBox1.Text = "World";
  32. }
  33. if (sender == this.button3)
  34. {
  35. this.textBox1.Text = "Mr.Okay";
  36. }
  37. }
  38. }

一个事件可以挂接多个事件处理器,一个事件处理器也可以被多个事件所挂接。

WPF 示例

WPF 可以在 XAML 里面绑定事件。
2020122事件详解 - 图5
它将自动在后台生成事件绑定代码。
2020122事件详解 - 图6
也可以在后台手动绑定事件:

  1. public partial class MainWindow : Window
  2. {
  3. public MainWindow()
  4. {
  5. InitializeComponent();
  6. //this.Button1.Click += this.ButtonClicked;
  7. this.Button1.Click += new RoutedEventHandler(this.ButtonClicked);
  8. }
  9. private void ButtonClicked(object sender, RoutedEventArgs e)
  10. {
  11. this.TextBox1.Text = "Hello, WASPEC!";
  12. }
  13. }

事件的声明

事件声明有完整声明和简略声明两种,简略声明是完整声明的语法糖
事件无论是从表层约束还是从底层实现都是依赖于委托的。

事件声明完整格式

注:声明委托类型(与类同级) 声明委托类型字段(在类内部)。

  1. using System;
  2. using System.Threading;
  3. namespace EventExample
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. // 1.事件拥有者
  10. Customer customer = new Customer();
  11. // 2.事件响应者
  12. Waiter waiter = new Waiter();
  13. // 3.Order 事件成员 5. +=事件订阅
  14. customer.Order += waiter.Action;
  15. customer.Action();
  16. customer.PayTheBill();
  17. }
  18. }
  19. // 该类用于传递点的是什么菜,作为事件参数,需要以 EventArgs 结尾,且继承自 EventArgs
  20. public class OrderEventArgs : EventArgs//传递事件消息的类
  21. {
  22. public string DishName { get; set; }
  23. public string Size { get; set; }
  24. }
  25. // 注意委托类型的声明和类声明是平级的
  26. public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
  27. /*使用EventHandler后缀的用意
  28. 1. 标记委托为声明事件;
  29. 2. 表名委托用于约束事件处理器
  30. 3. 委托未来创建的实例是用来存储事件处理器的*/
  31. public class Customer
  32. {
  33. // 委托类型字段
  34. private OrderEventHandler orderEventHandler;
  35. // 事件声明
  36. public event OrderEventHandler Order
  37. //事件公开程度 事件 委托约束事件 事件名
  38. {
  39. add //事件添加器
  40. {
  41. this.orderEventHandler += value;
  42. }
  43. remove //事件移除器
  44. {
  45. this.orderEventHandler -= value;
  46. }
  47. }
  48. public double Bill { get; set; }
  49. public void PayTheBill()
  50. {
  51. Console.WriteLine("I will pay ${0}.", this.Bill);
  52. }
  53. public void WalkIn()
  54. {
  55. Console.WriteLine("Walk into the restaurant");
  56. }
  57. public void SitDown()
  58. {
  59. Console.WriteLine("Sit down.");
  60. }
  61. public void Think()
  62. {
  63. for (int i = 0; i < 5; i++)
  64. {
  65. Console.WriteLine("Let me think ...");
  66. Thread.Sleep(1000);
  67. }
  68. if (this.orderEventHandler != null)//需要得到服务员的相应
  69. {
  70. OrderEventArgs e = new OrderEventArgs();
  71. e.DishName = "Kongpao Chicken";
  72. e.Size = "large";
  73. this.orderEventHandler.Invoke(this, e);
  74. }
  75. }
  76. public void Action()
  77. {
  78. Console.ReadLine();
  79. this.WalkIn();
  80. this.SitDown();
  81. this.Think();
  82. }
  83. }
  84. public class Waiter
  85. {
  86. // 4.事件处理器
  87. public void Action(Customer customer, OrderEventArgs e)
  88. {
  89. Console.WriteLine("I will serve you the dish - {0}.", e.DishName);
  90. double price = 10;
  91. switch (e.Size)
  92. {
  93. case "small":
  94. price *= 0.5;
  95. break;
  96. case "large":
  97. price *= 1.5;
  98. break;
  99. default:
  100. break;
  101. }
  102. customer.Bill += price;
  103. }
  104. }
  105. }

事件声明简略格式(字段式声明, field-like)

一种 filed-like 的声明格式。
filed-like:像字段声明一样 。

简略格式与上例的完整格式只有事件声明事件触发两处不同。

  1. using System;
  2. using System.Threading;
  3. namespace EventExample
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. // 1.事件拥有者
  10. Customer customer = new Customer();
  11. // 2.事件响应者
  12. Waiter waiter = new Waiter();
  13. // 3.Order 事件成员 5. +=事件订阅
  14. customer.Order += waiter.Action;
  15. customer.Action();
  16. customer.PayTheBill();
  17. }
  18. }
  19. public class OrderEventArgs : EventArgs
  20. {
  21. public string DishName { get; set; }
  22. public string Size { get; set; }
  23. }
  24. public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
  25. public class Customer
  26. {
  27. // 简略事件声明,看上去像一个委托(delegate)类型字段
  28. public event OrderEventHandler Order;
  29. public double Bill { get; set; }
  30. public void PayTheBill()
  31. {
  32. Console.WriteLine("I will pay ${0}.", this.Bill);
  33. }
  34. public void WalkIn()
  35. {
  36. Console.WriteLine("Walk into the restaurant");
  37. }
  38. public void SitDown()
  39. {
  40. Console.WriteLine("Sit down.");
  41. }
  42. public void Think()
  43. {
  44. for (int i = 0; i < 5; i++)
  45. {
  46. Console.WriteLine("Let me think ...");
  47. Thread.Sleep(1000);
  48. }
  49. if (this.Order != null)
  50. {
  51. OrderEventArgs e = new OrderEventArgs();
  52. e.DishName = "Kongpao Chicken";
  53. e.Size = "large";
  54. // 事件触发
  55. this.Order.Invoke(this, e);
  56. }
  57. }
  58. public void Action()
  59. {
  60. Console.ReadLine();
  61. this.WalkIn();
  62. this.SitDown();
  63. this.Think();
  64. }
  65. }
  66. public class Waiter
  67. {
  68. // 4.事件处理器
  69. public void Action(Customer customer, OrderEventArgs e)
  70. {
  71. Console.WriteLine("I will serve you the dish - {0}.", e.DishName);
  72. double price = 10;
  73. switch (e.Size)
  74. {
  75. case "small":
  76. price *= 0.5;
  77. break;
  78. case "large":
  79. price *= 1.5;
  80. break;
  81. default:
  82. break;
  83. }
  84. customer.Bill += price;
  85. }
  86. }
  87. }using System;
  88. using System.Threading;
  89. namespace EventExample
  90. {
  91. class Program
  92. {
  93. static void Main(string[] args)
  94. {
  95. // 1.事件拥有者
  96. Customer customer = new Customer();
  97. // 2.事件响应者
  98. var waiter = new Waiter();
  99. // 3.Order 事件成员 5. +=事件订阅
  100. customer.Order += waiter.Action;
  101. customer.Action();
  102. customer.PayTheBill();
  103. }
  104. }
  105. public class OrderEventArgs:EventArgs
  106. {
  107. public string DishName { get; set; }
  108. public string Size { get; set; }
  109. }
  110. public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
  111. public class Customer
  112. {
  113. // 简略事件声明,看上去像一个委托(delegate)类型字段
  114. public event OrderEventHandler Order;
  115. public double Bill { get; set; }
  116. public void PayTheBill()
  117. {
  118. Console.WriteLine("I will pay ${0}.",this.Bill);
  119. }
  120. public void WalkIn()
  121. {
  122. Console.WriteLine("Walk into the restaurant");
  123. }
  124. public void SitDown()
  125. {
  126. Console.WriteLine("Sit down.");
  127. }
  128. public void Think()
  129. {
  130. for (int i = 0; i < 5; i++)
  131. {
  132. Console.WriteLine("Let me think ...");
  133. Thread.Sleep(1000);
  134. }
  135. if (this.Order != null)
  136. {
  137. var e = new OrderEventArgs();
  138. e.DishName = "Kongpao Chicken";
  139. e.Size = "large";
  140. // 事件触发
  141. this.Order.Invoke(this,e);
  142. }
  143. }
  144. public void Action()
  145. {
  146. Console.ReadLine();
  147. this.WalkIn();
  148. this.SitDown();
  149. this.Think();
  150. }
  151. }
  152. public class Waiter
  153. {
  154. // 4.事件处理器
  155. public void Action(Customer customer, OrderEventArgs e)
  156. {
  157. Console.WriteLine("I will serve you the dish - {0}.",e.DishName);
  158. double price = 10;
  159. switch (e.Size)
  160. {
  161. case "small":
  162. price *= 0.5;
  163. break;
  164. case "large":
  165. price *= 1.5;
  166. break;
  167. default:
  168. break;
  169. }
  170. customer.Bill += price;
  171. }
  172. }
  173. }

使用 ildasm 反编译,查看隐藏在事件简化声明背后的秘密。
2020122事件详解 - 图7

委托类型字段能否替代事件

既然已有了委托类型字段/属性,为什么还要事件?
因为事件成员能让程序逻辑更加“有道理”、更加安全,谨防“借刀杀人”。

真正项目中,往往很多人在同一段代码上工作,如果在语言层面未对某些功能进行限值,这种自由度很可能被程序员滥用或误用。

像下面这种使用字段的方式,和 C、C++ 里面使用函数指针是一样的,经常出现函数指针指到了一个程序员不想调用的函数上去,进而造成逻辑错误。这也是为什么 Java 彻底放弃了与函数指针相关的功能 —— Java 没有委托类型。

正是为了解决 public 委托字段在类的外部被滥用或误用的问题,微软才推出了事件这个成员。

  1. using System;
  2. using System.Threading;
  3. namespace EventExample
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. Console.ReadLine();
  10. Customer customer = new Customer();
  11. Waiter waiter = new Waiter();
  12. customer.Order += waiter.Action;
  13. //customer.Action();
  14. // badGuy 借刀杀人,给 customer 强制点菜
  15. OrderEventArgs e = new OrderEventArgs();
  16. e.DishName = "Man han quan xi";
  17. e.Size = "large";
  18. OrderEventArgs e2 = new OrderEventArgs();
  19. e2.DishName = "Beer";
  20. e2.Size = "large";
  21. Customer badGuy = new Customer();
  22. badGuy.Order += waiter.Action;
  23. badGuy.Order.Invoke(customer, e);
  24. badGuy.Order.Invoke(customer, e2);
  25. customer.PayTheBill();
  26. }
  27. }
  28. public class OrderEventArgs : EventArgs
  29. {
  30. public string DishName { get; set; }
  31. public string Size { get; set; }
  32. }
  33. public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
  34. public class Customer
  35. {
  36. // 去掉 Event,把事件声明改成委托字段声明
  37. public OrderEventHandler Order;
  38. public double Bill { get; set; }
  39. public void PayTheBill()
  40. {
  41. Console.WriteLine("I will pay ${0}.", this.Bill);
  42. }
  43. public void WalkIn()
  44. {
  45. Console.WriteLine("Walk into the restaurant");
  46. }
  47. public void SitDown()
  48. {
  49. Console.WriteLine("Sit down.");
  50. }
  51. public void Think()
  52. {
  53. for (int i = 0; i < 5; i++)
  54. {
  55. Console.WriteLine("Let me think ...");
  56. Thread.Sleep(1000);
  57. }
  58. if (this.Order != null)
  59. {
  60. OrderEventArgs e = new OrderEventArgs();
  61. e.DishName = "Kongpao Chicken";
  62. e.Size = "large";
  63. this.Order.Invoke(this, e);
  64. }
  65. }
  66. public void Action()
  67. {
  68. Console.ReadLine();
  69. this.WalkIn();
  70. this.SitDown();
  71. this.Think();
  72. }
  73. }
  74. public class Waiter
  75. {
  76. public void Action(Customer customer, OrderEventArgs e)
  77. {
  78. Console.WriteLine("I will serve you the dish - {0}.", e.DishName);
  79. double price = 10;
  80. switch (e.Size)
  81. {
  82. case "small":
  83. price *= 0.5;
  84. break;
  85. case "large":
  86. price *= 1.5;
  87. break;
  88. default:
  89. break;
  90. }
  91. customer.Bill += price;
  92. }
  93. }
  94. }

一旦将 Order 声明为事件(添加 event 关键字),就能避免上面的问题。
QQ截图20220304142931.png :::danger 再次提醒:在Customer类内部能够使用Order事件去做非空比较以及调用Order.Invoke方法纯属不得己而为之.
因为使用事件的简化声明时,我们没有手动声明一个委托类型的字段。这是微软编译器语法糖所造成的语法冲突和前后不一致。

:::

事件的本质

事件的本质是一个蒙版,是委托字段的一个包装

蒙板 Mask: 事件这个包装器对委托字段的访问起限制作用,只让你访问 +=、-= ,让你只能给事件添加或移除事件处理器。让程序更加安全更好维护。

封装 Encapsulation: 上面的限制作用,就是面向对象的封装这个概念。 封装的一个重要功能就是隐藏,把一些东西封装隐藏起来,在外部只暴露我想让你看到的东西。

事件对外界隐藏了委托实例的大部分功能,仅暴露添加/移除事件处理器的功能。
添加/移除事件处理器的时候可以直接使用方法名,这是委托实例所不具备的功能

命名约定

  • 用于声明事件的委托类型的命名约定

    使用 EventHandler

    2020122事件详解 - 图9

    • 用于声明Foo事件的委托, 一般命名为Foo EventHandler(除非是一个非常通用的事件约束) ```csharp using System; using System.Threading;

namespace EventExample { class Program { static void Main(string[] args) { var customer = new Customer(); var waiter = new Waiter(); customer.Order += waiter.Action;

  1. customer.Action();
  2. customer.PayTheBill();
  3. }
  4. }
  5. public class OrderEventArgs : EventArgs
  6. {
  7. public string DishName { get; set; }
  8. public string Size { get; set; }
  9. }
  10. //public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
  11. public class Customer
  12. {
  13. // 使用默认的 EventHandler,而不是声明自己的
  14. public event EventHandler Order;
  15. public double Bill { get; set; }
  16. public void PayTheBill()
  17. {
  18. Console.WriteLine("I will pay ${0}.", this.Bill);
  19. }
  20. public void WalkIn()
  21. {
  22. Console.WriteLine("Walk into the restaurant");
  23. }
  24. public void SitDown()
  25. {
  26. Console.WriteLine("Sit down.");
  27. }
  28. public void Think()
  29. {
  30. for (int i = 0; i < 5; i++)
  31. {
  32. Console.WriteLine("Let me think ...");
  33. Thread.Sleep(1000);
  34. }
  35. if (this.Order != null)
  36. {
  37. var e = new OrderEventArgs();
  38. e.DishName = "Kongpao Chicken";
  39. e.Size = "large";
  40. this.Order.Invoke(this, e);
  41. }
  42. }
  43. public void Action()
  44. {
  45. Console.ReadLine();
  46. this.WalkIn();
  47. this.SitDown();
  48. this.Think();
  49. }
  50. }
  51. public class Waiter
  52. {
  53. public void Action(object sender, EventArgs e)
  54. {
  55. // 类型转换
  56. var customer = sender as Customer;
  57. var orderInfo = e as OrderEventArgs;
  58. Console.WriteLine("I will serve you the dish - {0}.", orderInfo.DishName);
  59. double price = 10;
  60. switch (orderInfo.Size)
  61. {
  62. case "small":
  63. price *= 0.5;
  64. break;
  65. case "large":
  66. price *= 1.5;
  67. break;
  68. default:
  69. break;
  70. }
  71. customer.Bill += price;
  72. }
  73. }

}

  1. - FooEventHandler委托的参数一般有两个(由Win32API演化而来, 历史悠久)
  2. - 第一个是object类型, 名字为sender 实际上就是事件的拥有者、事件的source
  3. - 第二个是EventArgs类的派生类, 类名一般为Foo EventArgs 参数名为e。也就是前面讲过的事件参数
  4. - 虽然没有官方的说法,但我们可以把委托的参数列表看做是事件发生后发送给事件响应者的“事件消息”
  5. <a name="gSfAi"></a>
  6. ### 专用于触发事件的方法
  7. 触发事件的方法一般命名为 OnXxx,且访问级别为 protected(自己的类成员及派生类能访问)。
  8. 依据单一职责原则,把原来的 Think 中触发事件的部分单独提取为 OnOrder 方法。
  9. ```csharp
  10. public void Think()
  11. {
  12. for (int i = 0; i < 5; i++)
  13. {
  14. Console.WriteLine("Let me think ...");
  15. Thread.Sleep(1000);
  16. }
  17. this.OnOrder("Kongpao Chicken","large");
  18. }
  19. protected void OnOrder(string dishName,string size)
  20. {
  21. if (this.Order != null)//使用前先检测是否为空
  22. {
  23. var e = new OrderEventArgs();
  24. e.DishName = dishName;
  25. e.Size = size;
  26. this.Order.Invoke(this, e);
  27. }
  28. }
  • 触发Foo事件的方法一般命名为OnFoo, 即”因何引发”、“事出有因”
    • 事件的触发由自己进行触发
    • 访问级别为protected, 不能为public, 不然又成了可以”借刀杀人”了

      • 事件的命名约定
  • 带有时态的动词或者动词短语
  • 事件拥有者“正在做”什么事情,用进行时;事件拥有者”做完了”什么事情,用完成时

    事件的命名约定

    注意动词的时态。
    2020122事件详解 - 图10

    事件与委托的关系

  • 事件真的是“以特殊方式声明的委托字段/实例”吗?
    • 不是!只是声明的时候“看起来像”(对比委托字段与事件的简化声明,field-like)
    • 事件声明的时候使用了委托类型,简化声明造成事件看上去像一个委托的字段(实例),而 event 关键字则更像是一个修饰符 —— 这就是错觉的来源之一
    • 订阅事件的时候 += 操作符后面可以是一个委托实例,这与委托实例的赋值方法语句相同,这也让事件看起来像是一个委托字段 —— 这是错觉的又一来源
    • 重申:事件的本质是加装在委托字段上的一个“蒙版”(mask),是个起掩蔽作用的包装器。这个用于阻挡非法操作的“蒙版”绝不是委托字段本身
  • 为什么要使用委托类型来声明事件?
    • 站在 source 的角度来看,是为了表明 source 能对外传递哪些消息
    • 站在 subscriber 的角度来看,它是一种约定,是为了约束能够使用什么样签名的方法来处理(响应)事件
    • 委托类型的实例将用于存储(引用)事件处理器
  • 对比事件与属性
    • 属性不是字段 —— 很多时候属性是字段的包装器,这个包装器用来保护字段不被滥用
    • 事件不是委托字段 —— 它是委托字段的包装器,这个包装器用来保护委托字段不被滥用
    • 包装器永远都不可能是被包装的东西

2020122事件详解 - 图11
在上图中是被迫使用事件去做 != 和 .Invoke(),学过事件完整声明格式,就知道事件做不了这些。在这里能这样是因为简略格式下事件背后的委托字段是编译器自动生成的,这里访问不了。
总结:事件不是委托类型字段(无论看起来多像),它是委托类型字段的包装器,限制器,从外界只能访问 += -= 操作。