15.1 发布者和订阅者
发布者 / 订阅者模式:发布者类定义了一系列程序的其他部分可能感兴趣的事件,其他一些订阅者类可以向发布者提供一个方法来“注册”以获取通知。当事件发生时,发布者“触发事件”,然后依次执行订阅者提交的所有事件。
实际上事件包含了一个私有的委托,对于事件我们只能添加、删除或调用事件处理程序。
15.2 源代码组件预览
- 委托类型声明;
- 事件处理程序声明;
- 事件声明;
- 事件注册;
- 触发事件的代码。
15.3 声明事件
public event [委托类型] [事件名];
public event EventHandler CountADozen; // 也可以加 static 使其变成静态的
15.4 订阅事件
类似委托,用+=
可以添加事件处理程序。
15.5 触发事件
触发事件之前要和null
进行比较,从而查看事件是否包含事件处理程序。
下面的代码包含了前 5 个小节的知识点,这段代码描述了一个玩家从坐标 (0,0) 开始直线走到 (100, 0),再走到 (100, 100),(0, 100),最后回到原点。每当他走到拐角(不包括出发时)的时候会触发一次事件,事件包含两个处理程序:报告当前位置以及模拟一次普通攻击。
using System;
namespace Test
{
delegate void Handler(Player player); // 声明委托
class Player
{
public int coordinateX, coordinateY; // 二维坐标
public event Handler AtTheCorner; // 创建事件并发布
public void MoveOneTrip() // 从 (0,0) 开始沿着正方形走一圈回到起点
{
coordinateX = coordinateY = 0;
while (++coordinateX < 100)
continue;
AtTheCorner?.Invoke(this); // 每当遇到转角时触发一次事件
while (++coordinateY < 100)
continue;
AtTheCorner?.Invoke(this); // ?. 运算符简化了判空
while (--coordinateX > 0)
continue;
AtTheCorner?.Invoke(this);
while (--coordinateY > 0)
continue;
AtTheCorner?.Invoke(this);
}
}
class PlayerShowInformation
{
public PlayerShowInformation(Player player)
{
player.AtTheCorner += SayHello; // 订阅事件
}
void SayHello(Player player) // 报告此时的坐标位置
{
Console.WriteLine("Hey, I am at the corner now!");
Console.WriteLine("My position is : {0}, {1}."
, player.coordinateX, player.coordinateY);
}
}
class PlayerAttack
{
public PlayerAttack(Player player)
{
player.AtTheCorner += NormalAttack; // 订阅事件
}
void NormalAttack(Player player) // 模拟普通攻击,攻击力为两个坐标之和
{
Console.WriteLine("Normal attack --- {0} damage.\n"
, player.coordinateX + player.coordinateY);
}
}
class Program
{
static void Main()
{
Player noobPlayer = new Player();
PlayerShowInformation noobPlayerInformation = new PlayerShowInformation(noobPlayer);
PlayerAttack noobPlayerAttack = new PlayerAttack(noobPlayer);
noobPlayer.MoveOneTrip(); // 触发事件
}
}
}
15.6 标准事件的用法
和上面的代码做了同样的事情,但这次使用了System
命名空间中声明的EventHandler
委托类型,以及扩展的EventArgs
来传递数据。
在事件的两个处理程序中使用了两种不同的传递数据的方法:
- 在第 45 行
SayHello
方法中用了EventHandler
的第一个参数,即触发事件的对象的引用,通过强制类型转换来得到对象成员的值; - 在第 59 行
NormalAttack
方法中用了泛型委托,使用自定义类MyEventArgs
来代替原本的第二个参数,并通过这个类来传递数据。
using System;
using System.Data;
namespace Test
{
public class MyEventArgs : EventArgs
{
public int X { get; set; }
public int Y { get; set; }
public MyEventArgs Update(int x, int y)
{
X = x;
Y = y;
return this;
}
}
class Player
{
MyEventArgs args = new MyEventArgs();
public int coordinateX, coordinateY;
public event EventHandler<MyEventArgs> AtTheCorner; // 泛型委托
public void MoveOneTrip()
{
coordinateX = coordinateY = 0;
while (++coordinateX < 100)
continue;
AtTheCorner?.Invoke(this, args.Update(coordinateX, coordinateY)); // 使用自定义的参数类
while (++coordinateY < 100)
continue;
AtTheCorner?.Invoke(this, args.Update(coordinateX, coordinateY));
while (--coordinateX > 0)
continue;
AtTheCorner?.Invoke(this, args.Update(coordinateX, coordinateY));
while (--coordinateY > 0)
continue;
AtTheCorner?.Invoke(this, args.Update(coordinateX, coordinateY));
}
}
class PlayerShowInformation
{
public PlayerShowInformation(Player player)
{
player.AtTheCorner += SayHello;
}
void SayHello(object source, MyEventArgs e)
{
Player player = (Player)source;
Console.WriteLine("Hey, I am at the corner now!");
Console.WriteLine("My position is : {0}, {1}."
, player.coordinateX, player.coordinateY);
}
}
class PlayerAttack
{
public PlayerAttack(Player player)
{
player.AtTheCorner += NormalAttack;
}
void NormalAttack(object source, MyEventArgs e)
{
Console.WriteLine("Normal attack --- {0} damage.\n", e.X + e.Y);
}
}
class Program
{
static void Main()
{
Player noobPlayer = new Player();
PlayerShowInformation noobPlayerInformation = new PlayerShowInformation(noobPlayer);
PlayerAttack noobPlayerAttack = new PlayerAttack(noobPlayer);
noobPlayer.MoveOneTrip();
}
}
}
15.7 事件访问器
事件只允许+=
和-=
运算符,但是我们可以为事件定义add
和move
两个事件访问器来改变两个运算符的操作。