image.png
事件并不是委托类型的字段!!!

初步了解事件

image.png

事件粗讲:能够发生,且能够通知到其它成员(重在效果)。

1656964477726.png

“订阅关系”粗讲:我关注着某件事…

事件的动作要素:

①有一个事件 ②有一群人关心(订阅)着这个事件 ③事件发生了 ④关心(订阅)的人们会被依次通知到 ⑤通知到的人们拿到事件参数进行响应

下图的概念是一个意思:(统一表示为 事件订阅、事件参数)
image.pngimage.png
拓展:

①设计模式如事件模式是不错的,但涉及到的元素比较多,在使用这个模式的时候不加以规划,容易一团乱麻。为了更好的规划,才出现了MVC、MVP、MVVM等模式。

事件的应用

image.png

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

用户按下按钮执行操作,看似是用户的外部操作引起按钮的 Click 事件触发,实际不然。
简言之:用户操作通过 Windows 调用了按钮的内部逻辑,最终还是按钮的内部逻辑触发了Click 事件。

事件示例

image.png表示“属性”。image.png表示“方法”。image.png表示“事件”。
(1)Timer的事件案例:(一个事件同时有两个事件处理器的案例)

效果:每隔一秒,同时打印一次Jump!和Sing!。

image.png

几种事件订阅方式

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

(14)事件详解 - 图11

  1. using System;
  2. using System.Windows.Forms;
  3. namespace EventExample
  4. {
  5. class Program{
  6. static void Main(string[] args){
  7. var form = new Form();// 1.事件拥有者
  8. var controller = new Controller(form);// 3.事件响应者
  9. form.ShowDialog();
  10. }
  11. }
  12. class Controller{
  13. private Form form;
  14. public Controller(Form form){
  15. if (form != null){
  16. this.form = form;
  17. // 2.事件成员 Click 5.事件订阅 +=
  18. this.form.Click += this.FormClicked;
  19. }
  20. }
  21. // 4.事件处理器
  22. private void FormClicked(object sender, EventArgs e){
  23. this.form.Text = DateTime.Now.ToString();
  24. }
  25. }
  26. }

为什么事件处理器中ElapsedEventArgs和EventArgs不同?

image.png

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

一个对象拿着自己的方法去订阅和处理自己的事件。
(14)事件详解 - 图13

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

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

这种是用得最广(常见)的:
(14)事件详解 - 图14

  1. using System;
  2. using System.Windows.Forms;
  3. namespace EventExample
  4. {
  5. class Program{
  6. static void Main(string[] args){
  7. // 3.事件响应者 form
  8. MyForm form = new MyForm();
  9. form.ShowDialog();
  10. }
  11. }
  12. class MyForm : Form{
  13. private TextBox textBox;
  14. // 1.事件拥有者 button
  15. private Button button;
  16. public MyForm(){
  17. this.textBox = new TextBox();
  18. this.button = new Button();
  19. this.Controls.Add(this.button);
  20. this.Controls.Add(this.textBox);
  21. // 2.事件成员 Click 5.事件订阅 +=
  22. this.button.Click += this.ButtonClicked;
  23. }
  24. // 4.事件处理器
  25. private void ButtonClicked(object sender, EventArgs e){
  26. this.textBox.Text = "Hello, World!!!!";
  27. }
  28. }
  29. }

WinForm,WPF 事件绑定示例

WinForm 示例
(14)事件详解 - 图15

  1. public Form1()
  2. {
  3. InitializeComponent();
  4. // 两种事件挂接的方式
  5. //this.button3.Click += this.ButtonClicked;
  6. //this.button3.Click += new EventHandler(this.ButtonClicked);
  7. // 挂接匿名方法
  8. // 已废弃的事件挂接方式(匿名挂接方法)(已过时,目前Lambda表达式)
  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 += (object sender, EventArgs e) => {...}
  15. this.button3.Click += (sender, e) =>
  16. {
  17. this.textBox1.Text = "Hoho";
  18. };
  19. }
  20. //事件处理器被重用的案例:
  21. //一个事件可以挂接过个处理器;一个处理器也可以被多个事件挂接。
  22. private void ButtonClicked(object sender, EventArgs e){
  23. if (sender == this.button1){ //重用1
  24. this.textBox1.Text = "Hello";
  25. }
  26. if (sender == this.button2){//重用2
  27. this.textBox1.Text = "World";
  28. }
  29. if (sender == this.button3){//重用3
  30. this.textBox1.Text = "Mr.Okay";
  31. }
  32. }

image.png
WPF 示例
WPF 可以在 XAML 里面绑定事件。
(14)事件详解 - 图17
它将自动在后台生成事件绑定代码。
(14)事件详解 - 图18
也可以在后台手动绑定事件(现代一点):

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

事件的声明

(14)事件详解 - 图19
●事件声明有完整声明简略声明两种,简略声明是完整声明的语法糖
●事件无论是从表层约束还是从底层实现都是依赖于委托的。

事件的完整声明格式

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

(1)EventHandler后缀的作用:(EventArgs后缀道理也差不多) 1.这个委托是专门用于声明事件的… 2.这个委托是用来约束事件处理器的… 3.这个委托未来创建出来的实例是专门用来存储事件处理器的…

  1. using System;
  2. namespace EventExample
  3. {
  4. class Program{
  5. static void Main(string[] args){
  6. var customer = new Customer();// 1.事件拥有者
  7. var waiter = new Waiter();// 2.事件响应者
  8. customer.Order += waiter.Action;// 3.Order 事件成员 5. +=事件订阅
  9. customer.PayTheBill();
  10. }
  11. }
  12. // 作为"事件参数",需要以 EventArgs 结尾,且继承自 EventArgs (此处该类用于传递点的是什么菜)
  13. public class OrderEventArgs : EventArgs{
  14. public string DishName { get; set; }
  15. public string Size { get; set; }
  16. }
  17. // 声明一个委托类型,需要以 EventHandler 结尾(用于事件处理)
  18. // 注意委托类型的声明和类声明是平级的(此处不是嵌套类)
  19. public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
  20. public class Customer{
  21. // 委托类型字段
  22. private OrderEventHandler orderEventHandler;//用来存储或引用事件处理器
  23. // 事件声明
  24. public event OrderEventHandler Order{
  25. add { this.orderEventHandler += value; }//事件处理器的添加器
  26. remove { this.orderEventHandler -= value; }//事件处理器的移除器
  27. }
  28. public double Bill { get; set; }
  29. public void PayTheBill(){
  30. if(this.orderEventHandler!=null){
  31. OrderEventArgs e = new OrderEventArgs();
  32. e.DishName = "Kongpao Chicken";
  33. e.Size = "large";
  34. this.orderEventHandler.Invoke(this, e);//此处为详细事件控制
  35. }
  36. Console.WriteLine("I will pay ${0}.", this.Bill);
  37. }
  38. }
  39. public class Waiter{
  40. public void Action(Customer customer, OrderEventArgs e){ // 4.事件处理器
  41. Console.WriteLine("I will serve you the dish - {0}.", e.DishName);
  42. double price = 10;
  43. switch (e.Size){
  44. case "small":
  45. price *= 0.5;
  46. break;
  47. case "large":
  48. price *= 1.5;
  49. break;
  50. default:
  51. break;
  52. }
  53. customer.Bill += price;
  54. }
  55. }
  56. }

事件的简略声明格式

①可以防止“借刀杀人”的方式…

事件的反编译视角:(绿色箭头:事件 蓝色方块:字段)
image.pngimage.png

  1. using System;
  2. using System.Threading;
  3. namespace EventExample
  4. {
  5. class Program{
  6. static void Main(string[] args){
  7. var customer = new Customer();// 1.事件拥有者
  8. var waiter = new Waiter();// 2.事件响应者
  9. customer.Order += waiter.Action;// 3.Order 事件成员 5. +=事件订阅
  10. customer.PayTheBill();
  11. }
  12. }
  13. // 作为"事件参数",需要以 EventArgs 结尾,且继承自 EventArgs (此处该类用于传递点的是什么菜)
  14. public class OrderEventArgs : EventArgs{
  15. public string DishName { get; set; }
  16. public string Size { get; set; }
  17. }
  18. // 声明一个委托类型,需要以 EventHandler 结尾(用于事件处理)
  19. // 注意委托类型的声明和类声明是平级的(此处不是嵌套类)
  20. public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
  21. public class Customer{
  22. public event OrderEventHandler Order;//简略后的"错觉"
  23. public double Bill { get; set; }
  24. public void PayTheBill(){
  25. if(this.Order!=null){
  26. OrderEventArgs e = new OrderEventArgs();
  27. e.DishName = "Kongpao Chicken";
  28. e.Size = "large";
  29. this.Order.Invoke(this, e);//此处为详细事件控制
  30. }
  31. Console.WriteLine("I will pay ${0}.", this.Bill);
  32. }
  33. }
  34. public class Waiter{
  35. public void Action(Customer customer, OrderEventArgs e){ // 4.事件处理器
  36. Console.WriteLine("I will serve you the dish - {0}.", e.DishName);
  37. double price = 10;
  38. switch (e.Size){
  39. case "small":
  40. price *= 0.5;
  41. break;
  42. case "large":
  43. price *= 1.5;
  44. break;
  45. default:
  46. break;
  47. }
  48. customer.Bill += price;
  49. }
  50. }
  51. }

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

既然已有了委托类型字段/属性,为什么还要事件?
因为事件成员能让程序逻辑更加“有道理”、更加安全,谨防“借刀杀人”。 真正项目中,往往很多人在同一段代码上工作,如果在语言层面未对某些功能进行限值,这种自由度很可能被程序员滥用或误用。 像下面这种使用字段的方式,和 C、C++ 里面使用函数指针是一样的,经常出现函数指针指到了一个程序员不想调用的函数上去,进而造成逻辑错误。这也是为什么 Java 彻底放弃了与函数指针相关的功能 —— Java 没有委托类型。 正是为了解决 public 委托字段在类的外部被滥用或误用的问题,微软才推出了事件这个成员。

事件的本质

事件的本质是一个蒙版。
蒙板 Mask: 事件这个包装器对委托字段的访问起限制作用,只让你访问 +=、-= ,让你只能给事件添加或移除事件处理器。让程序更加安全更好维护。
封装 Encapsulation: 上面的限制作用,就是面向对象的封装这个概念。把一些东西封装隐藏起来,在外部只暴露我想让你看到的东西。
事件对外界隐藏了委托实例的大部分功能,仅暴露添加/移除事件处理器的功能。

命名约定

(14)事件详解 - 图22

使用 EventHandler

.Net平台为我们准备好的。(使用默认的 EventHandler,而不是声明自己的)
(14)事件详解 - 图23
image.pngimage.png
image.png

专用于触发事件的方法

触发事件的方法一般命名为 OnXxx,且访问级别为 protected(自己的类成员及派生类能访问)。
依据单一职责原则,把原来的 Think 中触发事件的部分单独提取为 OnOrder 方法。

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

事件的命名约定

注意动词的时态。
(14)事件详解 - 图27

事件与委托的关系

image.png

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

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

总结:事件不是委托类型字段(无论看起来多像),它是委托类型字段的包装器,限制器,从外界只能访问 += -= 操作。
备注:与《C#图解教程》匹配食用,效果更佳。