事件的声明格式包括: 完整声明, 简略声明
简略声明是完整声明的语法糖衣,所以掌握事件的完整声明格式,可以了解事件声明的内部结构
事件的完整声明
实现:服务员 订阅,处理【点菜事件】
- 事件模型的5个组成部分
(1)事件的拥有者:顾客
(2)事件:点菜
(3)事件的响应者:服务员
(4)事件处理器:报菜单,记账
(5)订阅事件
思路
(1)准备事件的拥有者——类Customer
在该类中,有 double类型的【属性Bill】,用于存储饭菜花销
有返回值类型为 void 的【实例方法 PayTheBill()】,表示顾客付钱的动作
class Customer
{
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("Yummy!I will pay ${0}",this.Bill);
}
}
(2)声明【点菜事件】前应该做什么
明确:事件是基于委托的,这句话的含义是:
<1>事件需要委托类型来约束;该约束既规定了事件能够发送什么样的消息给事件的响应者,也规定了事件的响应者能够收到什么样的事件消息;这就决定了:事件响应者的事件处理器必须能够与该委托类型匹配上,它才能够订阅这个事件
<2>当事件响应者向事件拥有者提供了能够匹配事件的事件处理器之后,需要一个地方保存,记录事件处理器,而能够引用,记录方法的任务只有委托类型的实例能完成
所以,事件这个成员,无论是其表层约束,还是底层实现,都是依赖于委托类型的——“委托是事件的底层基础,事件是委托的上层建筑”
所以为了声明点菜事件,首先需为其准备一个委托
很显然C#类库中并没有为【点菜事件】准备好的委托,我们需要自己声明
(3)声明委托
.NET平台规定:当一个委托是为了声明某个事件所准备的,那么这个委托的名字应使用 【EventHandler】作为后缀
这么做的原因有三个:
<1>表明该委托是用来声明事件的,不是用来封装其他方法的,不应用它做其他的事情
<2>表明该委托是用来约束事件处理器的
<3>表明该委托未来创建的实例,是专门用于存储事件处理器的
所以,该委托的名字应为【OrderEventHandler】,这样的命名规则使得代码的可读性也增强了
//声明委托
pubic delegate void OrderEventHandler(Customer customer,OrderEventArgs e);
(4)声明【OrderEventArgs】
- 上述声明的委托【OrderEventHandler】,它所封装的目标方法的返回值是void,目标方法的第一个参数是 Customer类的实例,第二个参数是 OrderEventArgs类的实例(参数名一般简称为 “e”)
- 由于该委托是用来约束事件处理器的,所以它所封装的方法显然就是事件处理器
- 事件响应者的事件处理器是用来响应【点菜事件】的,它所要做的事情就是:报菜,记账
- 所以,它需要一个 Customer类的实例 作为参数,以便把账单记在顾客头上;需要一个 OrderEventArgs类的实例 作为参数,以便报菜并算账(第二个参数存储顾客所点菜的信息:菜名菜的份量)
- .NET平台规定:用来传递事件消息的类,命名规则应是类名加上EventArgs作为后缀(用意与后缀EventHandler同理),并且应让其派生自EventArgs类
- 应为其声明两个属性:string类型的 DishName,DishSize
class OrderEventArgs:EventArgs
{
public string DishName { get; set; }
public string DishSize { get; set; }
}
(5)声明委托类型的字段(是完整声明事件的一部分)
该字段是用来存储,引用事件处理器的;它是上面声明的【OrderEventHandler委托】的一个实例,并且也说过:该委托类型未来的实例,是专门用来存储事件处理器的
private OrderEventHandler ordereventhandler;
(6)声明【点菜(Order)事件】
所有的准备工作都已做好,就可以开始声明事件了
private OrderEventHandler ordereventhandler;
public event OrderEventHandler Order //OrderEventHandler是指拿哪个委托类型来约束该事件
{
add //添加事件处理器
{
this.ordereventhandler += value;
}
remove //移除事件处理器
{
this.ordereventhandler -= value;
}
}
"value" 是一个上下文关键字,指从外界传进来的事件处理器
之前在声明属性时也见到过 “value”
(7)创建事件的响应者——类Waiter
它的事件处理器在挂接时自动生成,事件处理器的逻辑应该是:
报菜——通过菜的分量计算菜的价钱——报账
(8)触发事件
明确:事件是不会主动发生的,它一定被事件拥有者的某些内部逻辑所触发
- 那么这个内部逻辑是什么呢?
- 模拟场景:顾客走进餐馆——坐下——思考吃什么——决定
- 决定的这个动作就触发了【点菜事件】
- 所以应先为类Customer添加一些实例方法,而且应在 Think() 方法中填写触发事件的逻辑
注意:
<1>this.ordereventhandler 的值也可能为空,说明没有事件处理器订阅该事件,所对应的场景就是:餐馆特忙,没服务员搭理你;那么如果在没有事件处理器订阅事件的情况下依然触发事件,就会得到异常;所以在此处需要判断这个用来存储或引用事件处理器的委托字段是否为空
<2>触发事件时使用的是委托的Invoke(),不能直接使用事件Order来触发,会报错;因为事件只能写在 “+=” 或 “-=” 操作符的左边
(9)打包
在Main()方法中依次调用WalkIn(),SitDown(),Think()方法太麻烦,既然它们是一连串的动作,就可以把它们放进一个 Action()方法,只需调用此方法,就可以执行这一连串的动作
public void Action()
{
this.WalkIn();
this.SitDown();
this.Think()
}
(10)回到程序主体,准备执行程序
完整代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += waiter.Waction;
customer.Action();
customer.PayTheBill();
}
}
public class OrderEventArgs:EventArgs //修改类的访问级别为public,因为这几个类未来会一起使用
{
public string DishName { get; set; }
public string DishSize { get; set; }
}
public delegate void OrderEventHandler(Customer customer,OrderEventArgs e);
public class Customer
{
private OrderEventHandler ordereventhandler;
public double Bill { get; set; }
public event OrderEventHandler Order
{
add
{
this.ordereventhandler += value;
}
remove
{
this.ordereventhandler -= value;
}
}
public void WalkIn()
{
Console.WriteLine("I am coming! I am so hungry!");
}
public void SitDown()
{
Console.WriteLine("I want a seat near the window,Thanks!");
Console.WriteLine("Let me sit down");
}
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("I am thinking-...");
Thread.Sleep(1000);
}
if (this.ordereventhandler != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "Ice-cream";
e.DishSize = "large";
this.ordereventhandler.Invoke(this, e);
}
}
public void Action()
{
this.WalkIn();
this.SitDown();
this.Think();
}
public void PayTheBill()
{
Console.WriteLine("Yummy!I will pay ${0}",this.Bill);
}
}
public class Waiter
{
internal void Waction(Customer customer, OrderEventArgs e)
{
Console.WriteLine("I will serve you - {0}",e.DishName);
double price = 10;
switch (e.DishSize)
{
case"large":
price = price * 1.5;
break;
case"small":
price = price * 0.5;
break;
default:
break;
}
customer.Bill = price;
Console.WriteLine("You should pay ${0} for this",price);
}
}
}
事件的简略声明
注意:虽然我们没有手动去声明委托字段,但它其实还在,打开反编译器就能看到,这个用以引用,存储事件处理器的委托字段被语法糖隐藏起来了
事件存在的必要性
- 疑问:既然可以声明委托类型的字段,为什么还需要事件这个成员?
- 回答:因为事件成员能够让程序逻辑以及对象之间的关系,变得更加 “有道理” ,更加安全,而且还能防止 “借刀杀人” 的情况出现
- 理解:如果现在不使用事件,把事件的简略声明格式中的 “event” 关键字删除,那么就真的成了一个委托类型的字段
public OrderEventHandler Order; //字段名为 Order
此时编译成功,程序依然可以正常运行;但由于该字段的访问级别是 【public】,就已经存在一些潜在的问题了
比如说:现在由 waiter 提供的事件处理器 Waction() 是被 customer 使用它的内部逻辑所调用的, 所以如果不去执行 customer.Action() ,理论上是不应该有别人去替 customer 点菜的
但实际情况却并不是这样
设想这样一个场景:有一个闲的没事干的坏家伙,专喜欢恶作剧,他就跑到前台假装是 customer 的随行,说:“给 customer 那桌再点个满汉全席!” 无辜的 customer 完全不知情,但菜已经做好了,只能认栽!
既然现在没有了【事件】,只有一个【可以随便从外界访问的委托字段Order】,那么坏家伙就可以利用这个字段给 customer 点菜了
- 注意:在真正的项目中,往往是很多人在同一段代码上工作,这时如果在语言层面上没有对某些功能进行限制,那么这种自由度就很有可能会被程序员们滥用,误用
- 像这种使用字段的方式与在 C++ 中使用函数指针的情况是完全相同的,经常不知道一个函数指针什么时候就指向了一个并不想调用中的函数上去,造成一些逻辑上的错误,而且这些错误很难debug,这就是Java中彻底放弃了与函数指针相关的功能的原因——Java没有委托类型
- 所以,正是为了解决这个 public 级别的委托字段有可能在类的外部被滥用或误用的问题,微软才推出了【事件】这种成员
public event OrderEventHandler Order;
此时的【Order】已经不是一个委托类型的字段了,而是一个【事件】,这个事件中包裹着一个委托类型的字段,此时再编译就会报错,错误提示事件只能写在 “+=” “-=” 操作符的左边;也就是说,要么为该事件挂接一个事件处理器,要么为该事件移除一个事件处理器,不能在这个事件拥有者的外部去随随便便触发该事件
- 提醒:
事件的本质
事件的本质是:委托字段的一个包装器,限制器
- 这个包装器对委托字段的访问起限制作用,相当于一个 “蒙版” ,只允许我们为委托字段添加,移除事件处理器,不能调用其他方法
- 也就是说,事件通过限制外界对委托字段的访问,隐藏了委托实例的大部分功能,比如说Invoke()方法
- 事件对委托字段的保护(限制作用——封装),使委托字段更加安全,也使整个程序更加好维护
命名约定
用于声明Foo事件的委托,一般命名为 【FooEventHandler】,除非是一个非常通用的事件约束,比如【EventHandler】
把【OrderEventHandler】替换为【EventHandler】,约束事件处理器的委托类型改变了,事件处理器也就需要做出相应的变化
既然使用的是这个微软早已为我们准备好的委托【EventHandler】,就无需再声明一个委托类型了
触发Foo事件的方法一般命名为【OnFoo】
在前面的代码中,触发事件的操作包含在 Think() 方法中,这违背了 “Single Responsibility” 原则,这样改进代码:
但是这样的话就永远只能点 大份的冰激凌了,希望可以在方法的外部决定点什么菜,以及菜的分量为多大,所以还需对代码进行改进
事件的命名应是带有时态的动词或动词短语