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

初步了解事件

020,021,022 事件详解 - 图1
以手机有响铃事件举列:

  • 手机可以通过响铃事件来通知关注手机的人

  • 响铃事件让手机具备了通知关注者的能力

  • 从手机角度看:

    • 响铃要求关注者采取行动

    • 通知关注者的同时,把相关消息也发送给关注者

  • 从人的角度看:

    • 人得到手机的通知,可以采取行动了

    • 除了得到通知,还收到了事件主体者(手机)经由事件发送过来的消息 事件参数 EventArgs

  • 响应事件:关注者得到通知后,检查事件参数,依据其内容采取响应的行动

    • 处理事件具体所做的事情:事件处理器 Event Handler

    • 如果是会议提醒:就去准备会议

    • 如果是电话接入:选择是否接听

    • 如果关注者在开会,直接抛弃掉事件参数,不做处理

事件的功能 = 通知 + 可选的事件参数(即详细信息)

术语约定

下面五种说法都指事件的订阅者:
020,021,022 事件详解 - 图2
下面四种说法都指事件参数:
020,021,022 事件详解 - 图3

事件模式

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

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

事件的应用

020,021,022 事件详解 - 图4

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

用户按下按钮执行操作,看似是用户的外部操作引起按钮的 Click 事件触发,实际不然,详细情况大致如下:

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

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

事件示例

Timer 的一些成员,其中闪电符号标识的两个就是事件:
020,021,022 事件详解 - 图5

通过查看 Timer 的成员,我们不难发现一个对象最重要的三类成员:

  • 属性:对象或类当前处于什么状态

  • 方法:它能做什么

  • 事件:它能在什么情况下通知谁

Timer Elapsed 事件示例:

  1. using System;
  2. using System.Timers;
  3. namespace EventExample
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. // 1.事件拥有者 timer
  10. var timer = new Timer();
  11. timer.Interval = 1000;
  12. // 3.事件的响应者 boy
  13. var boy = new Boy();
  14. var girl = new Girl();
  15. // 2.事件成员 Elapsed,5.事件订阅 +=
  16. timer.Elapsed += boy.Action;
  17. timer.Elapsed += girl.Action;
  18. timer.Start();
  19. Console.ReadLine();
  20. }
  21. }
  22. class Boy
  23. {
  24. // 这是通过 VS 自动生成的事件处理器,适合新手上手。
  25. // 4.事件处理器 Action
  26. internal void Action(object sender, ElapsedEventArgs e)
  27. {
  28. Console.WriteLine("Jump!");
  29. }
  30. }
  31. class Girl
  32. {
  33. internal void Action(object sender, ElapsedEventArgs e)
  34. {
  35. Console.WriteLine("Sing!");
  36. }
  37. }
  38. }

几种事件订阅方式

⭐事件拥有者和事件响应者是完全不同的两个对象

020,021,022 事件详解 - 图6

这种组合方式是 MVC、MVP 等设计模式的雏形。

Click 事件与上例的 Elapsed 事件的第二个参数的数据类型不同,即这两个事件的约定是不同的。

也就是说,你不能拿影响 Elapsed 事件的事件处理器去响应 Click 事件 —— 因为遵循的约束不同,所以他们是不通用的。

  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.事件拥有者
  10. var form = new Form();
  11. // 3.事件响应者
  12. var controller = new Controller(form);
  13. form.ShowDialog();
  14. }
  15. }
  16. class Controller
  17. {
  18. private Form form;
  19. public Controller(Form form)
  20. {
  21. if (form != null)
  22. {
  23. this.form = form;
  24. // 2.事件成员 Click 5.事件订阅 +=
  25. this.form.Click += this.FormClicked;
  26. }
  27. }
  28. // 4.事件处理器
  29. private void FormClicked(object sender, EventArgs e)
  30. {
  31. this.form.Text = DateTime.Now.ToString();
  32. }
  33. }
  34. }

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

一个对象拿着自己的方法去订阅和处理自己的事件。
020,021,022 事件详解 - 图7
该示例中事件的拥有者和响应者都是 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. var 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
  18. {
  19. // 4.事件处理器
  20. internal void FormClicked(object sender, EventArgs e)
  21. {
  22. this.Text = DateTime.Now.ToString();
  23. }
  24. }
  25. }

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

020,021,022 事件详解 - 图8
这种是用得最广的:

  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. var 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. }
  28. // 4.事件处理器
  29. private void ButtonClicked(object sender, EventArgs e)
  30. {
  31. this.textBox.Text = "Hello, World!!!!";
  32. }
  33. }
  34. }

WinForm,WPF 事件绑定示例

WinForm 示例

020,021,022 事件详解 - 图9

  1. public Form1()
  2. {
  3. InitializeComponent();
  4. // 两种事件挂接的方式
  5. //this.button3.Click += this.ButtonClicked;
  6. //this.button3.Click += new EventHandler(this.ButtonClicked);
  7. // 挂接匿名方法
  8. // 已废弃的事件挂接方式
  9. //this.button3.Click += delegate(object sender, EventArgs e)
  10. //{
  11. // this.textBox1.Text = "haha";
  12. //};
  13. // Lambda 表达式 PS:编译器可以通过委托约束推算出 sender 与 e 的数据类型
  14. this.button3.Click += (sender, e) =>
  15. {
  16. this.textBox1.Text = "Hoho";
  17. };
  18. }
  19. private void ButtonClicked(object sender, EventArgs e)
  20. {
  21. if (sender == this.button1)
  22. {
  23. this.textBox1.Text = "Hello";
  24. }
  25. if (sender == this.button2)
  26. {
  27. this.textBox1.Text = "World";
  28. }
  29. if (sender == this.button3)
  30. {
  31. this.textBox1.Text = "Mr.Okay";
  32. }
  33. }

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

WPF 示例

WPF 可以在 XAML 里面绑定事件。
020,021,022 事件详解 - 图10
它将自动在后台生成事件绑定代码。
020,021,022 事件详解 - 图11
也可以在后台手动绑定事件:

  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. }

事件的声明

020,021,022 事件详解 - 图12
事件声明有完整声明和简略声明两种,简略声明是完整声明的语法糖。

事件无论是从表层约束还是从底层实现都是依赖于委托的。

事件声明完整格式

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

  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. var customer = new Customer();
  11. // 2.事件响应者
  12. var 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. // 声明一个委托类型,因为该委托用于事件处理,所以以 EventHandler 结尾
  26. // 注意委托类型的声明和类声明是平级的
  27. public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
  28. public class Customer
  29. {
  30. // 委托类型字段
  31. private OrderEventHandler orderEventHandler;
  32. // 事件声明
  33. public event OrderEventHandler Order
  34. {
  35. add { this.orderEventHandler += value; }
  36. remove { this.orderEventHandler -= value; }
  37. }
  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.orderEventHandler != null)
  59. {
  60. var e = new OrderEventArgs();
  61. e.DishName = "Kongpao Chicken";
  62. e.Size = "large";
  63. this.orderEventHandler.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. // 4.事件处理器
  77. public void Action(Customer customer, OrderEventArgs e)
  78. {
  79. Console.WriteLine("I will serve you the dish - {0}.",e.DishName);
  80. double price = 10;
  81. switch (e.Size)
  82. {
  83. case "small":
  84. price *= 0.5;
  85. break;
  86. case "large":
  87. price *= 1.5;
  88. break;
  89. default:
  90. break;
  91. }
  92. customer.Bill += price;
  93. }
  94. }
  95. }

事件声明简略格式

一种 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. var customer = new Customer();
  11. // 2.事件响应者
  12. var 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. var 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. }

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

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

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

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

像下面这种使用字段的方式,和 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. var customer = new Customer();
  11. var waiter = new Waiter();
  12. customer.Order += waiter.Action;
  13. //customer.Action();
  14. // badGuy 借刀杀人,给 customer 强制点菜
  15. OrderEventArgs e = new OrderEventArgs();
  16. e.DishName = "Manhanquanxi";
  17. e.Size = "large";
  18. OrderEventArgs e2 = new OrderEventArgs();
  19. e2.DishName = "Beer";
  20. e2.Size = "large";
  21. var 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. var 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 关键字),就能避免上面的问题。
020,021,022 事件详解 - 图14
020,021,022 事件详解 - 图15

事件的本质

事件的本质是一个蒙版。

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

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

事件对外界隐藏了委托实例的大部分功能,仅暴露添加/移除事件处理器的功能。

命名约定

020,021,022 事件详解 - 图16

使用 EventHandler

020,021,022 事件详解 - 图17

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

专用于触发事件的方法

触发事件的方法一般命名为 OnXxx,且访问级别为 protected(自己的类成员及派生类能访问)。

依据单一职责原则,把原来的 Think 中触发事件的部分单独提取为 OnOrder 方法。

  1. public void Think()
  2. {
  3. for (int i = 0; i < 5; i++)
  4. {
  5. Console.WriteLine("Let me think ...");
  6. Thread.Sleep(1000);
  7. }
  8. this.OnOrder("Kongpao Chicken","large");
  9. }
  10. protected void OnOrder(string dishName,string size)
  11. {
  12. if (this.Order != null)
  13. {
  14. var e = new OrderEventArgs();
  15. e.DishName = dishName;
  16. e.Size = size;
  17. this.Order.Invoke(this, e);
  18. }
  19. }

事件的命名约定

注意动词的时态。
020,021,022 事件详解 - 图18

事件与委托的关系

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

020,021,022 事件详解 - 图19
在上图中是被迫使用事件去做 !=.Invoke(),学过事件完整声明格式,就知道事件做不了这些。在这里能这样是因为简略格式下事件背后的委托字段是编译器自动生成的,这里访问不了。

总结:事件不是委托类型字段(无论看起来多像),它是委托类型字段的包装器,限制器,从外界只能访问 += -= 操作。