奥运会参加百米的田径运动员听到枪声,比赛立即进行。其中枪声是事件,而运动员比赛就是这个事件发生后的动作。不参加该项比赛的人对枪声没有反应。
从程序的角度分析,假设这个场景里面有一个裁判(Referee),二个参赛运动员(Athlete),裁判开枪,之后会发出枪响,运动员听到枪响立刻跑步。我们抽象出:
1. 裁判:Referee
2. 裁判开枪:Shoot
3. 发出枪响:Gunshot
4. 运动员:Athlete
5. 运动员跑步:Run
这个场景可以用典型的观察者模式来实现,裁判(publisher)他会开枪发出枪响,所有的运动员(subscriber)听到枪响立刻跑步。我们使用委托来实现这个功能。
首先定义Referee:
/// <summary>
/// 裁判
/// </summary>
public class Referee
{
//裁判名字
private string _name;
public Referee(string name)
{
_name = name;
}
//定义枪声委托
public delegate void GunshotDelegate();
//声明枪声委托变量,所有运动员必须订阅这个变量,将来才可以听到“枪响”。
public GunshotDelegate Gunshot;
//裁判开枪
public void Shoot()
{
Console.WriteLine(_name+" shoot start the game:");
//枪响,所有聆听枪声的运动员开始跑步。(发布)
Gunshot();
}
}
然后定义Athlete:
/// <summary>
/// 运动员
/// </summary>
public class Athlete
{
//名字
private string _name;
public Athlete(string name)
{
_name = name;
}
//跑步
public void Run()
{
Console.WriteLine(_name + " begin to run.");
}
}
在客户端代码调试:
//初始化2位运动员
Athlete athlete1 = new Athlete("Athlete1");
Athlete athlete2 = new Athlete("Athlete2");
//初始化裁判
Referee referee = new Referee("Referee");
//运动员聆听枪声(订阅)
referee.Gunshot = athlete1.Run;
referee.Gunshot += athlete2.Run;
//裁判开枪(发布)
referee.Shoot();
我们对这个代码进行一个简单总结,在Observer设计模式中,主要包括两类对象:publisher和subscriber。
1. publisher并不需要关心有多少subscriber。
2. subscriber也不需要知道publisher什么时候会发布订阅。
3. 松耦合管理对象间的一种一对多的依赖关系。
4. 当publisher对象的状态改变时,subscriber对象会被自动告知并更新。
但是我们的用委托来实现存在不足。
方法注册不一致:
//运动员聆听枪声(订阅)
Referee referee = new Referee("Referee");
referee.Gunshot = athlete1.Run;
referee.Gunshot += athlete2.Run;
第一个方法注册用“=”,是赋值语法,因为要进行实例化,第二个方法注册则用的是“+=”。但是,不管是赋值还是注册,都是将方法绑定到委托上,除了调用时先后顺序不同,再没有任何的分别。
public的委托字段封装性不好
下面这两种方式都可以让比赛开始,但这不是我们愿意看到的,在客户端可以对它进行随意的赋值和调用等操作,严重破坏对象的封装性。
referee.Shoot();//调用开枪方法
referee.Gunshot();//直接调用枪声委托字段
如果把委托字段定义成private,客户端对它根本就不可见,所以必须手动显示实现委托的Add和Remove方法,比较麻烦。
所以因为这些缺点,我们才有了event关键字。它封装了委托类型的变量,使得:在类的内部,不管你声明它是public还是protected,它总是private的。在类的外部,注册“+=”和注销“-=”的访问限定符与你在声明事件时使用的访问符相同。但是通常我们都声明public。我们修改上面的代码:
//声明枪声委托变量,所有运动员必须订阅这个变量,将来才可以听到“枪响”
public event GunshotDelegate Gunshot;
事件的声明与之前委托变量的声明唯一的区别是多了一个event关键字。但是却解决了之前我们越到的问题:
1.赋值问题
referee.Gunshot = athlete1.Run; //编译错误
修改为:
referee.Gunshot += athlete1.Run;
2.封装问题
referee.Gunshot();//编译错误,不可以访问private成员
使用反编译工具查看event关键字背后的秘密:
事件声明之后的委托被编译成私有字段,并同时生成了Add和Remove方法。这两个方法分别用于注册委托类型的方法和取消注册。实际上也就是: “+= ”对应 add_XXX,“-=”对应remove_XXX。在add_XXX()方法内部,实际上调用了System.Delegate的Combine()静态方法,这个方法用于将当前的变量添加到委托链表中。
.Net Framework中的委托与事件
尽管上面的范例很好地完成了我们想要完成的工作,但是我们不仅疑惑:为什么.Net Framework 中的事件模型和上面的不同?为什么有很多的EventArgs参数?
我们先搞懂 .Net Framework的编码规范:
1.委托类型的名称都应该以EventHandler结束。
2.委托的原型定义:有一个void返回值,并接受两个输入参数:一个Object类型,一个EventArgs类型(或继承自EventArgs)。
3.事件的命名为 委托去掉 EventHandler之后剩余的部分。
4.继承自EventArgs的类型应该以EventArgs结尾。
5.委托声明原型中的Object类型的参数代表了sender,就是publisher。
6.EventArgs对象包含了subscriber所感兴趣的数据。
现在我们改写之前的范例,让它符合 .Net Framework 的规范:
/// <summary>
/// 裁判
/// </summary>
public class Referee
{
//裁判名字
private string _name;
public Referee(string name)
{
_name = name;
}
//定义枪声委托
public delegate void GunshotEventHandler(object sender, EventArgs args);
//声明枪声委托变量,所有运动员必须订阅这个变量,将来才可以听到“枪响”
public event GunshotEventHandler Gunshot;
//裁判开枪
public void Shoot()
{
Console.WriteLine(_name+" shoot start the game:");
//枪响,所有聆听枪声的运动员开始跑步
Gunshot(this, EventArgs.Empty);
}
}
/// <summary>
/// 运动员
/// </summary>
public class Athlete
{
//名字
private string _name;
public Athlete(string name)
{
_name = name;
}
//跑步
public void Run(object sender, EventArgs args)
{
Console.WriteLine(_name + " begin to run.");
}
}
static void Main(string[] args)
{
//初始化2位运动员
Athlete athlete1 = new Athlete("Athlete1");
Athlete athlete2 = new Athlete("Athlete2");
//初始化裁判
Referee referee = new Referee("Referee");
//运动员聆听枪声
referee.Gunshot += athlete1.Run;
referee.Gunshot += athlete2.Run;
//裁判开枪
referee.Shoot();
Console.ReadKey();
}
输出为:
总结
通过文章学到了委托作为字段来实现观察者模式的不足,使用event可以改善,以及.Net Framework的事件编码规范。