发布者和订阅者
很多程序都有一个共同的需求,既当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。
发布者/订阅者模式(publisher/subscriber pattern)可以满足这种需求:
- 发布者类定义了事件成员。
- 订阅者注册在事件成员被触发时要调用的回调方法(事件处理程序)。
- 当发布者触发事件时,执行订阅者提供的回调方法。
发布者(publisher):发布某个事件的类或结构,其他类可以在该事件发生时得到通知。
订阅者(subscriber):注册并在事件发生时得到通知的类或结构。
事件处理程序(event handler):由订阅者注册到事件的方法,在发布者触发事件时执行。事件处理程序方法可以定义在事件所在的类或结构中,也可以定义在不同的类或结构中。
触发(raise)事件:调用或触发事件的术语。当事件触发时,所有注册到它的方法都会被依次调用。
源代码组件概览
需要在事件中使用的代码有5部分。
- 委托类型声明。事件和事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述。
- 事件处理程序声明。订阅者类会在事件触发时执行的方法声明。它们不一定是有显式命名的方法,还可以是匿名方法或Lambda表达式。
- 事件声明。发布者类必须声明一个订阅者类可以注册的事件成员。当声明的事件为public时,称为发布了事件。
- 事件注册。订阅者必须订阅事件才能在它被触发时得到通知。
- 触发事件的代码。发布者类中“触发”事件并导致调用注册的所有事件处理程序的代码。
声明事件
发布者类必须提供事件对象。创建事件比较简单,只需要委托类型和名字。
- 事件声明在一个类中。
- 它需要委托类型的名称,任何附加到事件(如注册)的处理程序都必须与委托类型的签名和返回类型匹配。
- 它声明为public,这样其他类和结构可以在它上面注册事件处理程序。
- 不能使用对象创建表达式(new表达式)来创建它的对象。
class Incrementer { 关键字 委托类型 事件名 ↓ ↓ ↓ public event EventHandler CountedADozen; }
|
可以通过使用逗号分隔同时声明多个事件。
public event EventHandler MyEvent1, MyEvent2, OtherEvent;
|
还可以使用static关键字让事件变成静态的。
public static event EventHandler CountedADozen;
|
事件是成员
事件不是类型,事件是类或结构的成员。
- 不能在一段可执行代码中声明事件。
- 必须声明在类或结构中。
- 事件成员被隐式初始化为null。
- 事件声明需要委托类型的名字。
- BCL声明了一个叫做EventHandler的委托,专门用于系统事件。
订阅事件
订阅者向事件添加事件处理程序。
使用+=运算符来为事件增加事件处理程序。事件处理程序位于该运算符的右边。
事件处理程序的规范可以是以下任意一种:
- 实例方法的名称
- 静态方法的名称
- 匿名方法
- Lambda表达式
为CountedADozen事件增加3个方法。
incrementer.CountedADozen += IncrementDozensCount; incrementer.CountedADozen += ClassB.CounterHandlerB; mc.CountedADozen += new EventHandler(cc.CounterHandlerC);
|
Lambda表达式和匿名方法。
incrementer.CountedADozen += () => DozensCount++; incrementer.CountedADozen += delegate { DozensCount++; };
|
触发事件
在触发事件之前和null进行比较,从而查看是否包含事件处理程序,如果事件是null,则不能执行。
- 使用事件名称,后面跟的参数列表包含在圆括号中。
- 参数列表必须与事件的委托类型相匹配。
if (CountedADozen != null) { CountedADozen (source, args); }
|
完整示例
- 在构造函数中,Dozens类订阅事件,将IncrementDozensCount作为事件处理程序。
- 在Incrementer类的DoCount方法中,每增长12个数就触发CountedADozen事件。
delegate void Handler();
class Incrementer { public event Handler CountedADozen; public void DoCount() { for (int i = 1; i < 100; i++) { if (i % 12 == 0 && CountedADozen != null) { CountedADozen(); } } } }
class Dozens { public int DozensCount { get; private set; } public Dozens(Incrementer incrementer) { DoZensCount = 0; incrementer.CountedADozen += IncrementDozensCount; } void IncremtentDozensCount() { DozensCount++; } }
class Program { static void Main() { Incrementer incrementer = new Incrementer(); Dozens dozensCounter = new Dozens(incrementer); incrementer.DoCount(); Console.WriteLine("Number of dozens = {0}", dozensCounter.DozensCount); } }
|
标准事件的用法
程序事件的异步处理是使用C#事件的绝佳场景。WIndows GUI编程广泛地使用了事件。对于事件的使用,.NET框架提供了一个标准模式:EventHandler委托类型。
public delegate void EventHandler(object sender, EventArgs e);
|
- 第一个参数用来保存触发事件的对象的引用(object类型,可以匹配任何类型的实例)。
- 第二个参数用来保存状态信息,指明什么类型适用于该应用程序。
- 返回类型为void。
- EventArgs不能传递任何数据。如果希望传递数据,必须声明一个派生自EventArgs的类。
使用EventHandler委托
- 在声明中使用系统定义的EventHandler委托替换Handler。
- 订阅者中声明的事件处理程序的签名必须与事件委托(object和EventArgs参数)的签名和返回类型匹配。
- 触发事件的代码在调用事件时必须使用适当的参数类型的对象。
class Incrementer { public event EventHandler CountedADozen;
public void DoCount() { for (int i = 1; i < 100; i++) { if (i % 12 == 0 && CountedADozen != null) { CountedADozen(this, null); } } } }
class Dozens { public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer) { DozensCount = 0; incrementer.CountedADozen += (object sender, EventArgs e) => { DozensCount++; }; } }
class Program { static void Main(string[] args) { Incrementer incrementer = new Incrementer(); Dozens dozensCounter = new Dozens(incrementer); incrementer.DoCount(); Console.WriteLine("Number of dozens = {0}", dozensCounter.DozensCount); Console.ReadKey(); } }
|
通过扩展EventArgs来传递数据
为了向事件处理程序的第二个参数传入数据,需要声明一个派生自EventArgs的自定义类,可以保存要传入的数据。类的名称应该以EventArgs结尾。
声明一个自定义类IncrementerEventArgs,将字符串存储在IterationCount字段中。
public class IncrementerEventArgs : EventArgs { public int IterationCount { get; set; } }
|
泛型委托使用自定义类。
public event EventHandler<IncrementerEventArgs> CountedADozen;
|
使用自定义类IncrementerEventArgs和泛型委托EventHandler<IncrementerEventArgs>。
public class IncrementerEventArgs : EventArgs { public int IterationCount { get; set; } }
public class Incrementer { public event EventHandler<IncrementerEventArgs> CountedADozen;
public void DoCount() { IncrementerEventArgs args = new IncrementerEventArgs(); for (int i = 1; i < 100; i++) { if (i % 12 == 0 && CountedADozen != null) { args.IterationCount = i; CountedADozen(this, args); } } } }
public class Dozens { public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer) { DozensCount = 0; incrementer.CountedADozen += IncrementDozensCount; }
private void IncrementDozensCount(object source, IncrementerEventArgs e) { Console.WriteLine("Incremented at iteration: {0} in {1}", e.IterationCount, source.ToString() ); DozensCount++; } }
class Program { static void Main(string[] args) { Incrementer incrementer = new Incrementer(); Dozens dozensCounter = new Dozens(incrementer); incrementer.DoCount(); Console.WriteLine("Number of dozens = {0}", dozensCounter.DozensCount); Console.ReadKey(); } }
|
输出结果:
移除事件处理程序
用完事件处理程序之后,可以使用-=运算符把事件处理程序从事件中移除。
p.SimpleEvent -= s.MethodB;
|
完整程序实例。
class Publisher { public event EventHandler SimpleEvent; public void RaiseTheEvent() { SimpleEvent(this, null); } }
class Subscriber { public void MethodA(object o, EventArgs e) { Console.WriteLine("AAA"); } public void MethodB(object o, EventArgs e) { Console.WriteLine("BBB"); } }
class Program { static void Main() { Publisher p = new Publisher(); Subscriber s = new Subscriber(); p.SimpleEvent += s.MethodA; p.SimpleEvent += s.MethodB; p.RaiseTheEvent(); Console.WriteLine("\r\nRemove MethodB"); p.SimpleEvent -= s.MethodB; p.RaiseTheEvent(); } }
|
输出结果:
注意:如果一个处理程序向事件注册了多次,那么当移除处理程序时,只移除列表中处理程序的最后一个实例。