思考并回答以下问题:
- 为什么要使用事件?事件的主体是发布者怎么理解?
- 什么是回调方法和事件处理程序?
- 事件使你无法直接访问委托怎么理解?
- 事件被触发时发生了什么?
- 事件(event)可以声明为private吗?
- 事件可以申明为静态吗?怎么使用?委托可以为静态吗?为什么?
- 事件是类型吗?如果不是那是什么?事件可以声明在类或结构外面吗?
- 事件的初始值是什么?
- EventHandler是干嘛用的?
- 多播委托多种方法同样的参数怎么理解?
本章涵盖:
- 1.发布者和订阅者
- 2.源代码组件概览
- 3.声明事件
- 4.订阅事件
- 5.触发事件
- 6.标准事件的用法
- 7.事件访问器
发布者和订阅者
很多程序都有一个共同的需求,即当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。
你对钱的增加感兴趣,然后我负责监控,每增加12块钱,我就去做你交给我的事情,但你需要先把事情写在我给你的本子上。
反了,上面陈述的主体是订阅者,主体应该是发布者才对。我想叫两个人帮我算钱,他们先把计数的按钮放在我的工具箱里,我每增加12块钱,我就打开工具箱按下按钮,他们接收到消息就各自进行处理。
发布者/订阅者模式(publisher/subscriber pattern)可以满足这种需求。在这种模式中,发布者类定义了一系列程序的其他部分可能感兴趣的事件。其他类可以“注册”,以便在这些事件发生时发布者可以通知它们。这些订阅者类通过向发布者提供一个方法来“注册”以获取通知。当事件发生时,发布者“触发事件”,然后执行订阅者提交的所有事件。
由订阅者提供的方法称为回调方法,因为发布者通过执行这些方法来“往回调用订阅者的方法”。还可以将它们称为事件处理程序,因为它们是为处理事件而调用的代码。图1演示了这个过程,展示了拥有一个事件的发布者以及该事件的三个订阅者。
图1 发布者和订阅者
下面是一些有关事件的重要事项。
- 发布者(publisher) 发布某个事件的类或结构,其他类可以在该事件发生时得到通知。
- 订阅者(subscriber)注册并在事件发生时得到通知的类或结构。
- 事件处理程序(event handler) 由订阅者注册到事件的方法,在发布者触发事件时执行。事件处理程序方法可以定义在事件所在的类或结构中,也可以定义在不同的类或结构中。
- 触发(raise)事件 调用(invoke)或触发(fire)事件的术语。当事件触发时,所有注册到它的方法都会被依次调用。
事件的很多部分都与委托类似。实际上,事件就像是专门用于某种特殊用途的简单委托。委托和事件的行为之所以相似,是有充分理由的。事件包含了一个私有的委托,如图2所示。
图2 事件有被封装的委托
有关事件的私有委托需要了解的重要事项如下。
- 事件提供了对它的私有控制委托的结构化访问:也就是说,你无法直接访问委托。
- 事件中可用的操作比委托要少,对于事件我们只可以添加、删除或调用事件处理程序。
- 事件被触发时,它调用委托来依次调用调用列表中的方法。
注意,在图2中,只有+=和-=运算符在事件框的左边。因为,它们是事件唯一允许的操作(除了调用事件本身)。
图3演示了一个叫做Incrementer(Publisher)的类,它按照某种方式进行计数。
- Incrementer定义了一个CountedADozen事件,每次累积到12个项时将会触发该事件。
- 订阅者类Dozens和SomeOtherClass(Subscriber)各有一个注册到CountedADozen事件的事件处理程序。
- 每当触发事件时,都会调用这些处理程序。
图3 具有一个事件的类的结构和术语
源代码组件概览
需要在事件中使用的代码有5部分,如图4所示。这些组件如下所示。
- 委托类型声明 事件和事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述。
- 事件处理程序声明 订阅者类中会在事件触发时执行的方法声明。它们不一定是有显式命名的方法,还可以是匿名方法或Lambda表达式。
- 事件声明 发布者类必须声明一个订阅者类可以注册的事件成员。当声明的事件为public时,称为发布了事件。
- 事件注册 订阅者必须订阅事件才能在它被触发时得到通知。
- 触发事件的代码 发布者类中“触发”事件并导致调用注册的所有事件处理程序的代码。
图4 使用事件时的5个源代码组件
声明事件
发布者类必须提供事件对象。创建事件比较简单——只需要委托类型和名字。事件声明的语法如下代码所示,代码中声明了一个叫做CountADozen的事件。注意如下有关CountedADozen事件的内容。
- 事件声明在一个类中。
- 它需要委托类型的名称,任何附加到事件(如注册)的处理程序都必须与委托类型的签名和返回类型匹配。
- 它声明为public,这样其他类和结构可以在它上面注册事件处理程序。
- 不能使用对象创建表达式(new表达式)来创建它的对象。
1 | class Incrementer |
EventHandler是系统定义的委托,原型如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\mscorlib.dll
using System.Runtime.InteropServices;
namespace System
{
// 摘要:
// 表示将处理不包含事件数据的事件的方法。
//
// 参数:
// sender:
// 事件源。
//
// e:
// 不包含任何事件数据的 System.EventArgs。
[ ]
[ ]
public delegate void EventHandler(object sender, EventArgs e);
}
我们可以通过使用逗号分隔的列表在一个声明语句中声明一个以上的事件。例如,下面语句声明了3个事件。1
public event EventHandler MyEvent1, MyEvent2, OtherEvent;
我们还可以使用static关键字让事件变成静态的,如下声明所示:1
public static event EventHandler CountedADozen;
事件是成员
一个常见的误解是把事件认为是类型,然而它不是。和方法、属性一样,事件是类或结构的成员,这一点引出了几个重要的特性。
- 由于事件是成员:
- 我们不能在一段可执行代码中声明事件;
- 它必须声明在类或结构中,和其他成员一样。
- 事件成员被隐式自动初始化为null。
事件声明需要委托类型的名字,我们可以声明一个委托类型或使用已存在的。如果我们声明一个委托类型,它必须指定事件保存的方法的签名和返回类型。
BCL声明了一个叫做EventHandler的委托,专门用于系统事件。
订阅事件
订阅者向事件添加事件处理程序。对于一个要添加到事件的事件处理程序来说,它必须具有与事件的委托相同的返回类型和签名。
- 使用+=运算符来为事件增加事件处理程序,如下面代码所示。事件处理程序位于该运算符的右边。
- 事件处理程序的规范可以是以下任意一种:
- 实例方法的名称;
- 静态方法的名称;
- 匿名方法;
- Lambda达式。
例如,下面代码为CountedADozen事件增加了3个方法:
第一个是使用方法形式的实例方法;
第二个是使用方法形式的静态方法;
第三个是使用委托形式的实例方法。
1 | // incrementer是类名,CountedADozen是事件成员 |
和委托一样,我们可以使用匿名方法和Lambda表达式来添加事件处理程序。例如,如下代码先使用Lambda表达式然后使用了匿名方法。1
2
3
4// Lambda表达式
incrementer.CountedADozen += () => DozensCount++;
// 匿名方法
incrementer.CountedADozen += delegate { DozensCount++; };
触发事件
事件成员本身只是保存了需要被调用的事件处理程序。如果事件没有被触发,什么都不会发生。我们需要确保在合适的时候有代码来做这件事情。
例如,如下代码触发了CountedADozen事件。注意如下有关代码的事项。
- 在触发事件之前和null进行比较,从而查看是否包含事件处理程序,如果事件是null,则表示没有,不能执行。
- 触发事件的语法和调用方法一样:
- 使用事件名称,后面跟的参数列表包含在圆括号中;
- 参数列表必须与事件的委托类型相匹配。
1 | // CountedADozen 事件名 |
把事件声明和触发事件的代码放在一起便有了如下的发布者类声明。这段代码包含了两个成员:事件和一个叫做DoCount的方法,它将在适当的时候触发该事件。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Incrementer
{
public event EventHandler CountADozen; // 创建事件并发布
public void DoCount(object source, EventArgs args)
{
int i;
for (i = 1; i < 101; i++)
{
if (i % 12 == 0 && CountADozen != null) // 确认有方法可以执行
{
CountADozen(source, args); // 触发事件
}
}
}
}
下面的代码展示了整个程序,包含发布者类Incrementer和订阅者类Dozens。代码需要注意的地方如下:
- 在构造函数中,Dozens类订阅事件,将IncrementDozensCount作为事件处理程序;
- 在Incrementer类的DoCount方法中,每增长12个数就触发CountedADozen事件。
包含发布者和订阅者的完整程序,展示了使用事件所必需的5个部分
发布者Publisher Incrementer.cs
1 | using System; |
订阅者Subscriber Dozens.cs
1 | using System; |
Program.cs
1 | using System; |
产生如下的结果:1
Number of dozens = 8
标准事件的用法
GUI编程是事件驱动的,也就是说在程序运行时,它可以在任何时候被事件打断,比如按钮点击、按下按键或系统定时器。在这些情况发生时,程序需要处理事件然后继续其他事情。
显然,程序事件的异步处理是使用C#事件的绝佳场景。Windows GUI编程如此广泛地使用了事件,对于事件的使用,.NET框架提供了一个标准模式。事件使用的标准模式的根本就是System命名空间声明的EventHandler委托类型。EventHandler委托类型的声明如下代码所示。1
public delegate void EventHandler(object sender, EventArgs e);
关于该声明需要注意以下几点:
- 第一个参数用来保存触发事件的对象的引用。由于是object类型的,所以可以匹配任何类型的实例;
- 第二个参数用来保存状态信息,指明什么类型适用于该应用程序;
- 返回类型是void。
EventHandler委托类型的第二个参数是EventArgs类的对象,它声明在System命名空间中。你可能会想,既然第二个参数用于传递数据,EventArgs类的对象应该可以保存一些类型的数据。你可能错了。
- EventArgs设计为不能传递任何数据。它用于不需要传递数据的事件处理程序——通常会被忽略。
- 如果你希望传递数据,必须声明一个派生自EventArgs的类,使用合适的字段来保存需要传递的数据。
尽管EventArgs类实际上并不传递数据,但它是使用EventHandler委托模式的重要部分。不管参数使用的实际类型是什么,object类和EventArgs总是基类。这样EventHandler就能提供一个对所有事件和事件处理器都通用的签名,只允许两个参数,而不是各自都有不同签名。
如下代码所示,我们修改Incrementer程序使之使用EventHandler委托。注意以下几点。
- 在声明中使用系统定义的EventHandler委托替换Handler.
- 订阅者中声明的事件处理程序的签名必须与事件委托(现在使用object和EventArgs参数)的签名(和返回类型)匹配。对于IncrementDozensCount事件处理程序来说,该方法忽略了正式的参数。
- 触发事件的代码在调用事件时必须使用适当的参数类型的对象。
发布者Publisher Increment.cs
1 | using System; |
订阅者Subscriber Dozens.cs
1 | using System; |
Program.cs
1 | using System; |
事件访问器
自己的总结
事件只能用+=
委托在类里面
别的类.委托字段 调用不安全
事件是由委托进化来的。
订阅
可以往里面装方法,但是没有办法执行。只有类才能执行。所以事件只能定义在类里面。
事件的出现是一个渐进的过程,从安全性出发进行修改从而得到了订阅机制。
委托写在命名空间下和写在类里是不同的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17using System;
using System.Collections.Generic;
namespace Test
{
public delegate void MyDel();
class Tool
{
public MyDel del;
}
class Weapon
{
public MyDel del2;
}
}
此时的Program.cs1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32using System;
using System.Collections.Generic;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Tool tool1 = new Tool();
// 多播委托
tool1.del = Show;
tool1.del += Run;
// 执行委托
tool1.del();
Console.ReadKey();
}
public static void Show()
{
Console.WriteLine("Show被调用了");
}
public static void Run()
{
Console.WriteLine("Run被调用了");
}
}
}
此时产生了什么问题呢?即1
tool1.del();
不安全,任意一个类new Tool后都可以执行这个委托,而这个委托是属于Tool的,本该由Tool类来执行。
这边可以在Tool里加一个方法1
2
3
4
5
6
7
8
9
10
11
12class Tool
{
public MyDel del;
public void ProcessDel()
{
if (del != null)
{
del();
}
}
}
则Program可以这样执行1
tool1.ProcessDel();
但是tool1.del();还可以执行,又不能把它变成private,变成私有的则tool1.del = Show;tool1.del += Run;就失效了。
这时候该事件出场了,Tool中给del变量加一个event关键字1
public event MyDel del;
此时Program报错如下:
无法直接调用tool1.del()了。这时候是想要的结果,逼着你只能使用tool1.ProcessDel();来执行委托,也就是用委托所在的类来执行,保证安全性。
接着注释掉tool1.del()这一行,添加tool1.ProcessDel();,还是报错
因为事件只能用+=号,最后修改如下:
Tool.cs
1 | using System; |
Program.cs
1 | using System; |
这样就实现了订阅。
通过扩展EventArgs来传递数据
为了向自己的事件处理程序的第二个参数传入数据,并且又符合标准惯例,我们需要声明一个派生自EventArgs的自定义类,它可以保存我们需要传入的数据。类的名称应该以EventArgs结尾。例如,如下代码声明了一个自定义类,它能将字符串存储在名称为Message的字段中1
2
3
4
5// IncrementerEventArgs是自对应类,EventArgs是基类
public class IncrementerEventArgs : EventArgs
{
public int IterationCount{ get; set; } // 存储整数
}
现在我们有了一个自定义的类,可以对事件处理程序的第二个参数传递数据,你需要一个使用新自定义类的委托类型。要获得该类,可以使用泛型版本的委托EventHandler<>。第17章将详细介绍C#泛型,所以现在你只需要观察。要使用泛型委托,需要做到以下两点,随后的代码也表达了这个意思。
- 将自定义类的名称放在尖括号内。
- 在需要使用自定义委托类型的时候使用整个字符串。例如,event声明可能为如下形式:
1
2
3// 泛型委托使用自定义类
// CountedADozen是事件名称
public event EventHandler<IncrementerEventArgs> CountedADozen;
下面我们在处理事件的其他4部分代码中使用自定义类和自定义委托。例如,下面的代码更新了Incrementer,使用自定义的EventArgs类IncrementerEventArgs和泛型EventHandlerk\1
2