事件
发布者/订阅者模式
很多程序都有一个共同的需求,即当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。
发布者/订阅者模式(publisher/subscriberpattern)可以满足这种需求。在这种模式中,发布者类定义了一系列程序的其他部分可能感兴趣的事件。其他类可以“注册”,以便在这些事件发生时收到发布者的通知。这些订阅者类通过向发布者提供一个方法来“注册”以获取通知。当事件发生时,发布者“触发事件”,然后执行订阅者提交的所有事件。
由订阅者提供的方法称为回调方法,因为发布者通过执行这些方法来“往回调用订阅者的方法”。还可以将它们称为事件处理程序,因为它们是为处理事件而调用的代码。
- 发布者(publisher):发布某个事件的类或结构,其他类可以在该事件发生时得到通知。
- 订阅者(subscriber):注册并在事件发生时得到通知的类或结构。
- 事件处理程序(eventhandler):由订阅者注册到事件的方法,在发布者触发事件时执行。事件处理程序方法可以定义在事件所在的类或结构中,也可以定义在不同的类或结构中。
- 触发(raise)事件:调用(invoke)或触发(fire)事件的术语。当事件被触发时,所有注册到它的方法都会被依次调用。
之前介绍了委托。事件的很多部分都与委托类似。实际上,事件就像是专门用于某种特殊用途的简单委托。委托和事件的行为之所以相似,是有充分理由的。事件包含了一个私有的委托
- 事件提供了对它的私有控制委托的结构化访问。也就是说,你无法直接访问委托
- 事件中可用的操作比委托要少,对于事件我们只可以添加、删除或调用事件处理程序。
- 事件被触发时,它调用委托来依次调用调用列表中的方法。
下图演示了一个叫作Incrementer的类;它按照某种方式进行计数。
- Incrementer定义了一个CountedADozen事件,每次累积到12个项时将会触发该事件
- 订阅者类Dozens和SomeOtherclass各有一个注册到CountedADozen事件的事件处理程序
- 每当触发事件时,都会调用这些处理程序。
5部分组成
上图需要由5部分组成:
- 委托类型声明: 事件和事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述。
- 事件处理程序声明: 订阅者类中会在事件触发时执行的方法声明。它们不一定是显式命名的方法,还可以是匿名方法或Lambda表达式。
- 事件声明: 发布者类必须声明一个订阅者类可以注册的事件成员。当类声明的事件为public时,称为 [b]发布了事件。
- 事件注册: 订阅者必须注册事件才能在事件被触发时得到通知。这是将事件处理程序与事件相连的代码。
- 触发事件的代码: 发布者类中“触发”事件并导致调用注册的所有事件处理程序的代码。
No.1 声明事件
发布者类必须提供事件对象。创建事件比较简单,只需要委托类型和名称。
class Incrementer{
// 声明事件,事件名为 CountedADozen
public event EventHandler CountedADozen;
// 可以声明多个事件
//public event EventHandler CountedADozen1,CountedADozen2;
// 让事件变成静态
//public static event EventHandler CountedADozen;
}
一个常见的误解是把事件视为类型,然而它不是。和方法、属性一样,事件是类或结构的成员,
- 由于事件是成员:
- 我们不能在一段可执行代码中声明事件;
- 它必须声明在类或结构中,和其他成员一样。
- 事件成员被隐式自动初始化为null。
事件声明需要委托类型的名称,我们可以声明一个委托类型或使用已有的委托类型。如果声明一个委托类型,它必须指定将被事件注册的方法的签名和返回类型。
No.2 订阅事件
订阅者向事件添加事件处理程序。对于一个要添加到事件的事件处理程序来说,它必须具有与事件的委托相同的返回类型和签名。
incrementer.CountedADozen += IncrementDozensCount; // 方法引用形式
incrementer.CountedADozen += ClassB.CounterHandlerB; // 静态方法引用形式
mc.CountedADozen += new EventHandler(cc.CounterHandlerC); // 委托形式
incrementer.CountedADozen += () => DozensCount++; // Lambda 形式
incrementer.CountedADozen += delegate { DozensCount++; }; // 匿名方法形式
No.3 触发事件
事件成员本身只是保存了需要被调用的事件处理程序。如果事件没有被触发,什么都不会发生。我们需要确保有代码在合适的时候做这件事情。
如下代码出发了CountedADozen事件:
- 在触发事件之前和null进行比较,从而查看事件是否包含事件处理程序。如果事件是null,则表示没有事件处理程序,不能执行。
- 触发事件的语法和调用方法一样:
- 使用事件名称,后面跟着参数列表(包含在圆括号中):
- 参数列表必须与事件的委托类型相匹配。
if(CountedADozen !=null){ // 确认有方法可以执行
CountedADozen(source, args); // 触发事件
}
下面代码展示了整个程序,包含发布者类Incrementer和订阅者类Dozens
发布者:
delegate void EventHandler(object sender, EventArgs args); // 声明委托
class Incrementer
{
// 声明事件并发布
public event EventHandler CountedADozen;
public void DoCount()
{
for (int i = 1; i < 100; i++)
{
if (i % 12 == 0)
{
// 确认有方法可以执行
if (CountedADozen != null)
{
// 触发事件:每增加12个计数触发事件一次
CountedADozen(this, args);
}
}
}
}
}
订阅者:
class Dozens
{
public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount; // 订阅事件
}
// 声明事件处理程序
void IncrementDozensCount(object source, EventArgs args)
{
DozensCount++;
}
}
class Program {
void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozensCounter = new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine(dozensCounter.DozensCount);
}
}
扩展EventArgs来传递数据
移除事件处理程序
事件访问器