发布者和订阅者

很多程序都有一个共同的需求,即当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。
发布者/订阅者模式(publisher/subscriber pattern)可以满足这种需求。在这种模式中,发布者类定义了一系列程序的其他部分可能感兴趣的事件。其他类可以 “注册”,以便在这些事件发生时收到发布者的通知。这些订阅者类通过向发布者提供一个方法来 “注册” 以获取通知。当事件发生时,发布者 “触发事件”,然后执行订阅者提交的所有事件。

由订阅者提供的方法称为回调方法,因为发布者通过执行这些方法来 “往回调用订阅者的方法”。还可以将它们称为事件处理程序,因为它们是为处理事件而调用的代码。

  • 发布者(publisher):发布某个事件的类或结构,其他类可以在该事件发生时得到通知。
  • 订阅者(subscriber):注册并在事件发生时得到通知的类或结构。
  • 事件处理程序(event handler):由订阅者注册到事件的方法,在发布者触发事件时执行。事件处理程序方法可以定义在事件所在的类或结构中,也可以定义在不同的类或结构中。
  • 触发(raise)事件:调用(invoke)或触发(fire)事件的术语。当事件被触发时,所有注册到它的方法都会被依次调用。

事件的很多部分都与委托类似。实际上,事件就像是专门用于某种特殊用途的简单委托。事件包含了一个私有的委托


![[csharp-12-event-siyou.png]]

  • 事件提供了对它的私有控制委托的结构化访问。也就是说,你无法直接访问委托。
  • 事件中可用的操作比委托要少,对于事件我们只可以添加、删除或调用事件处理程序。
  • 事件被触发时,它调用委托来依次调用调用列表中的方法。

源代码组件概览

  • 委托类型声明:事件和事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述。
  • 事件处理程序声明:订阅者类中会在事件触发时执行的方法声明。它们不一定是显式命名的方法,还可以是匿名方法或Lambda表达式。
  • 事件声明:发布者类必须声明一个订阅者类可以注册的事件成员。当类声明的事件为public时,称为发布了事件。
  • 事件注册:订阅者必须注册事件才能在事件被触发时得到通知。这是将事件处理程序与事件相连的代码。
  • 触发事件的代码:发布者类中 “触发” 事件并导致调用注册的所有事件处理程序的代码。

事件声明

发布者类必须提供事件对象。创建事件比较简单——只需要委托类型和名称。

  • 事件声明在一个类中。
  • 它需要委托类型的名称,任何附加到事件(如注册)的处理程序都必须与委托类型的签名和返回类型匹配。
  • 它声明为public,这样其他类和结构可以在它上面注册事件处理程序。
  • 不能使用对象创建表达式(new 表达式)来创建它的对象。
1
2
3
4
5
class Incrementer
{
//event关键字 EventHandler委托类型 CountedADozen事件名
public event EventHandler CountedADozen;
}

可以通过使用逗号分隔的列表在一个声明语句中声明一个以上的事件。

1
public event EventHandler MyEvent1, MyEvent2, OtherEvent;

还可以使用 static 关键字让事件变成静态的。

1
public static event EventHandler CountedADozen;

事件是成员

和方法、属性一样,事件是类或结构的成员,而不是一种类型。

  • 由于事件是成员:
    • 我们不能在一段可执行代码中声明事件;
    • 它必须声明在类或结构中,和其他成员一样。
  • 事件成员被隐式自动初始化为null
    事件声明需要委托类型的名称,我们可以声明一个委托类型或使用已有的委托类型。如果声明一个委托类型,它必须指定将被事件注册的方法的签名和返回类型。
    BCL声明了一个叫作 EventHandler 的委托,专门用于系统事件。

订阅事件

订阅者向事件添加事件处理程序。对于一个要添加到事件的事件处理程序来说,它必须具有与事件的委托相同的返回类型和签名。

  • 使用+=运算符来为事件添加事件处理程序,事件处理程序位于该运算符的右边。
  • 事件处理程序的规范可以是以下任意一种:
    • 实例方法的名称;
    • 静态方法的名称;
    • 匿名方法;
    • Lambda 表达式。

下面的代码为 CountedADozen 事件添加了 3 个方法:第一个是实例方法,第二个是实例静态方法,第三个是使用委托形式的实例方法。

1
2
3
4
incrementer.CountedADozen += IncrementDozensCount;        //方法引用形式
incrementer.CountedADozen += ClassB.CounterHandlerB; //方法引用形式

mc.CountedADozen += new EventHandler(cc.CounterHandlerC); //委托形式

和委托一样,我们可以使用匿名方法和Lambda表达式来添加事件处理程序。

1
2
3
4
//Lambda 表达式
incrementer.CountedADozen += () => DozensCount++;
//匿名方法
incrementer.CountedADozen += delegate { DozensCount++; };

触发事件

事件成员本身只是保存了需要被调用的事件处理程序。如果事件没有被触发,什么都不会发生。我们需要确保有代码在合适的时候做这件事情。

  • CountedADozen 事件在触发事件之前和 null 进行比较,从而查看事件是否包含事件处理程序。如果事件是 null,则表示没有事件处理程序,不能执行。
  • 触发事件的语法和调用方法一样:
    • 使用事件名称,后面跟着参数列表(包含在圆括号中);
    • 参数列表必须与事件的委托类型相匹配。
1
2
3
4
5
if (CountedADozen != null)        //确认有方法可以执行
CountedADozen(source, args); //触发事件

//事件名称: CountedADozen
//参数列表: (source, args)

把事件声明和触发事件的代码放在一起便有了如下的发布者类声明。这段代码包含了两个成员:事件和一个叫作 DoCount 的方法,该方法将在适当的时候触发该事件。

1
2
3
4
5
6
7
8
9
10
11
12
class Incrementer
{
public event EventHandler CountedADozen; //声明事件

void DoCount(object source, EventArgs args)
{
for( int i = 1; i < 100; i++ )
if( i % 12 == 0 )
if (CountedADozen != null) //确认有方法可以执行
CountedADozen(source, args); //触发事件
}
}

如下代码为一个完整程序,包含发布者类 Incrementer 和订阅者类 Dozens。代码中需要注意的地方如下:

  • 在构造函数中,Dozens 类订阅事件,将 IncrementDozensCount 作为事件处理程序;
  • Incrementer 类的 DoCount 方法中,每增加 12 个计数就触发 CountedADozen 事件。
1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
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(); //每增加12个计数触发事件一次
}
}

//订阅者
class Dozens
{
public int DozensCount { get; private set; }

public Dozens( Incrementer incrementer )
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount; //订阅事件
}

void IncrementDozensCount() //声明事件处理程序
{
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 );
}
}
1
Number of dozens = 8

标准事件的用法

GUI编程是事件驱动的,也就是说在程序运行时,它可以在任何时候被事件打断,比如按钮点击、按下按键或系统定时器。在这些情况发生时,程序需要处理事件然后继续做其他事情。

显然,程序事件的异步处理是使用C#事件的绝佳场景。Windows GUI编程如此广泛地使用了事件,以至于对于事件的使用,.NET框架提供了一个标准模式。该标准模式的基础就是System命名空间中声明的EventHandler委托类型。EventHandler委托类型的声明如以下代码所示。关于该声明需要注意以下几点。

  • 第一个参数用来保存触发事件的对象的引用。由于它是object类型的,所以可以匹配任何类型的实例。
  • 第二个参数用来保存状态信息,指明什么类型适用于该应用程序。
  • 返回类型是void
1
public delegate void EventHandler(object sender, EventArgs e);

EventHandler委托的第二个参数是EventArgs类的对象,它声明在System命名空间中。

  • EventArgs不能传递任何数据。它用于不需要传递数据的事件处理程序——通常会被忽略
  • 如果你希望传递数据,必须声明一个派生自EventArgs的类,并使用合适的字段来保存需要传递的数据。

尽管EventArgs实际上并不传递数据,但它是使用EventHandler委托的模式的重要部分。不管参数的实际类型是什么,objectEventArgs类型的参数总是基类。这样EventHandler就能提供一个对所有事件和事件处理器都通用的签名,让所有事件都正好有两个参数,而不是各自都有不同的签名。

如下代码,修改Incrementer程序,使之使用EventHandler委托。注意以下几点。

  • 在声明中使用系统定义的EventHandler委托替换Handler
  • 订阅者类中的事件处理程序声明的签名必须与事件委托(现在使用objectEventArgs类型的参数)的签名(和返回类型)匹配。对于IncrementDozensCount事件处理程序来说,该方法忽略了形参。
  • 触发事件的代码在调用事件时必须使用适当的参数类型的对象。
1
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
32
33
34
35
36
37
38
39
40
41
42
//发布者
class Incrementer
{
public event EventHandler CountedADozen; //使用系统定义的EventHandler委托

public void DoCount()
{
for (int i=1; i < 100; i++ )
if ( i % 12 == 0 && CountedADozen != null )
CountedADozen(this, null); //触发事件时使用EventHandler的参数
}
}

//订阅者
class Dozens
{
public int DozensCount { get; private set; }

public Dozens( Incrementer incrementer )
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount;
}

void IncrementDozensCount(object source, EventArgs e) //事件处理程序签名必须匹配委托
{
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 );
}
}
1
Number of dozens = 8

通过扩展EventArgs来传递数据

为了向自己的事件处理程序的第二个参数传入数据,同时遵循标准惯例,我们需要声明一个派生自EventArgs的自定义类,它可以保存我们需要传入的数据。类的名称应该以EventArgs结尾。

1
2
3
4
5
//自定义类,派生自基类EventArgs
public class IncrementerEventArgs : EventArgs
{
public int IterationCount { get; set; } //存储一个整数
}

现在我们有了一个自定义的类,可以向事件处理程序的第二个参数传递数据,需要一个使用新自定义类的委托类型。为此,可以使用泛型版本的委托EventHandler<>。要使用泛型委托,需要做到以下两点:

  • 将自定义类的名称放在尖括号内。
  • 在需要使用自定义委托类型的地方使用整个字符串。例如,event声明可能为如下形式:
1
2
3
//泛型委托使用自定义类
public event EventHandler<IncrementerEventArgs> CountedADozen;
// ↑事件名称
1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//自定义类派生自EventArgs
public class IncrementerEventArgs : EventArgs
{
public int IterationCount { get; set; } //存储一个整数
}

//发布者类
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 ); //在触发事件时传递参数
}
}
}
}

//订阅者类
class Dozens
{
public int DozensCount { get; private set; }

public Dozens( Incrementer incrementer )
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount;
}

void IncrementDozensCount( object source, IncrementerEventArgs e )
{
Console.WriteLine
($"Incremented at iteration: { e.IterationCount } in { source.ToString() }");
DozensCount++;
}
}

class Program
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozensCounter = new Dozens( incrementer );

incrementer.DoCount();
Console.WriteLine($"Number of dozens = { dozensCounter.DozensCount }");
}
}
1
2
3
4
5
6
7
8
9
Incremented at iteration: 12 in Counter.Incrementer
Incremented at iteration: 24 in Counter.Incrementer
Incremented at iteration: 36 in Counter.Incrementer
Incremented at iteration: 48 in Counter.Incrementer
Incremented at iteration: 60 in Counter.Incrementer
Incremented at iteration: 72 in Counter.Incrementer
Incremented at iteration: 84 in Counter.Incrementer
Incremented at iteration: 96 in Counter.Incrementer
Number of dozens = 8

移除事件处理程序

在用完事件处理程序之后,可以从事件中把它移除。可以利用-=运算符把事件处理程序从事件中移除.

1
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
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();
}
}
1
2
3
4
5
AAA
BBB

Remove MethodB
AAA

如果一个处理程序向事件注册了多次,那么当执行命令移除处理程序时,将只移除列表中该处理程序的最后一个实例。

事件访问器(高级主题,简单了解)

之前提到过,事件只能许+=-=运算符。这两个运算符有定义良好的行为。

然而,可以修改这两个运算符的行为,在使用它们时让事件执行任何我们希望执行的自定义代码。

要改变这两个运算符的操作,必须为事件定义事件访问器

  • 有两个访问器:addremove
  • 声明事件的访问器看上去和声明一个属性差不多。

下面的示例演示了具有访问器的事件声明。两个访问器都有叫作value的隐式值参数,它接受实例或静态方法的引用。

1
2
3
4
5
6
7
8
9
10
11
public event EventHandler CountedADozen
{
add
{
... //执行+=运算符的代码
}
remove
{
... //执行-=运算符的代码
}
}

声明了事件访问器之后,事件不包含任何内嵌委托对象。我们必须实现自己的机制来存储和移除事件注册的方法。

事件访问器表现为void方法,也就是不能使用返回值的return语句。