C#图解教程之事件

作者 Zhendong Ho 日期 2019-07-23
C#
C#图解教程之事件

发布者和订阅者

很多程序都有一个共同的需求,既当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。

发布者/订阅者模式(publisher/subscriber pattern)可以满足这种需求:

  1. 发布者类定义了事件成员。
  2. 订阅者注册在事件成员被触发时要调用的回调方法(事件处理程序)。
  3. 当发布者触发事件时,执行订阅者提供的回调方法。

14-1

发布者(publisher):发布某个事件的类或结构,其他类可以在该事件发生时得到通知。

订阅者(subscriber):注册并在事件发生时得到通知的类或结构。

事件处理程序(event handler):由订阅者注册到事件的方法,在发布者触发事件时执行。事件处理程序方法可以定义在事件所在的类或结构中,也可以定义在不同的类或结构中。

触发(raise)事件:调用或触发事件的术语。当事件触发时,所有注册到它的方法都会被依次调用。

源代码组件概览

需要在事件中使用的代码有5部分。

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

14-4

声明事件

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

  • 事件声明在一个类中。
  • 它需要委托类型的名称,任何附加到事件(如注册)的处理程序都必须与委托类型的签名和返回类型匹配。
  • 它声明为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++; // Lambda表达式
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(); // 触发事件,每增加12个计数触发一次
}
}
}
}

// 订阅者类
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); // 8
}
}

标准事件的用法

程序事件的异步处理是使用C#事件的绝佳场景。WIndows GUI编程广泛地使用了事件。对于事件的使用,.NET框架提供了一个标准模式:EventHandler委托类型

public delegate void EventHandler(object sender, EventArgs e);	// 系统定义的委托类型
  • 第一个参数用来保存触发事件的对象的引用(object类型,可以匹配任何类型的实例)。
  • 第二个参数用来保存状态信息,指明什么类型适用于该应用程序。
  • 返回类型为void。
  • EventArgs不能传递任何数据。如果希望传递数据,必须声明一个派生自EventArgs的类。

使用EventHandler委托

  • 在声明中使用系统定义的EventHandler委托替换Handler。
  • 订阅者中声明的事件处理程序的签名必须与事件委托(object和EventArgs参数)的签名和返回类型匹配。
  • 触发事件的代码在调用事件时必须使用适当的参数类型的对象。
/// <summary>
/// 发布者
/// </summary>
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); // 使用适当的参数
}
}
}
}

/// <summary>
/// 订阅者
/// </summary>
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); // 8
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  // 自定义类派生自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();
}
}

输出结果:

14-6-1

移除事件处理程序

用完事件处理程序之后,可以使用-=运算符把事件处理程序从事件中移除

p.SimpleEvent -= s.MethodB;  // 移除事件处理程序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; // 注册MethodA到事件
p.SimpleEvent += s.MethodB;
p.RaiseTheEvent();

Console.WriteLine("\r\nRemove MethodB");
p.SimpleEvent -= s.MethodB; // 把MethodB从事件中移除
p.RaiseTheEvent();
}
}

输出结果:

14-6-2

注意:如果一个处理程序向事件注册了多次,那么当移除处理程序时,只移除列表中处理程序的最后一个实例