一、自定义事件的完整声明方式
餐馆点餐Order这样一个事件:服务员拿自己的一个事件处理器来订阅顾客点餐这样一个事件。顾客的点餐这个事件一发生,服务员的事件处理器就会马上执行。
如何自定义一个事件?首先考虑构成事件的五个部分:事件拥有者,事件,事件响应者,事件处理器,订阅。
第一步:定义一个事件拥有者。
Customer类,他会有一个属性:账单Bill。一个方法:PayTheBill。
class Customer
{
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}.",this.Bill);
}
}
第二步:定义事件:
上一节提到:
我拿方法订阅你的一个事件,事件发生,执行这个方法。我把这个方法绑定事件。
这个函数方法相当于一个参数传递给某个接受值的对象。
事件是基于委托的:两层含义:
1、事件需要使用委托类型来做一个约束,即规定了事件要发送什么样的消息给事件的响应者,也规定了事件响应者能收到什么样的消息,(这两句话一个意思)。这就决定了事件响应者的事件处理器必须能够跟这个约束匹配上,他才能订阅这个事件。(用类型来限制约束。而声明委托就必须限制函数签名。能完成这个目的。)
2、当事件的响应者向事件的拥有者提供了能够匹配的事件处理器之后,需要将这个事件处理器记录或者保存下来,能够记录或者引用方法的任务,只有委托类型的实例才能够做到。(也就是说)
事件这个成员无论从表层约束来讲,还是底层实现来讲,都是依赖于委托类型的。委托是事件的底层基础,事件是委托的上层建筑。
声明事件类型:如果想要声明事件类型,就必须有一个合适委托类型来供我们使用。先声明一个委托类型。委托是一种类,所以其声明类的位置跟其他类是平级的。
// 委托类型,声明事件所需。
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
// 事件拥有者的类
class Customer
{
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}.",this.Bill);
}
}
// 传递事件消息的类
class OrderEventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}
按照Dotnet的规定,如果委托是为了声明某个事件而准备的,这个委托的命名方式必须是事件名+EventHandler后缀。所以定名为OrderEventHandler。
委托引用的方法参数列表,第一个参数:谁点的菜,事件的拥有者;第二个参数:存储有关点的菜的信息的,也就是要传递事件信息(EventArgs)的。Dotnet规定,这个类型是事件名+EventArgs。
DotNet规定事件的后缀EventHandler有三个用意:
1、标明委托是专门用来声明事件的,这样读到这个代码就知道用途,见名知意。
2、标明委托是用来约束事件处理器的。
3、标明这个委托未来创建出的实例是专门用来存储事件处理器的。可读性增强。
后缀EventArgs同样如此。
DotNet也规定,如果某个类的用途是作为EventArgs来使用,那么应该派生自EventArgs基类。微软准备好的基类。
委托类型的声明如果放入了类体中,编译可以通过,是因为C#支持嵌套类型的。但是只能类内使用。
此时就可以声明委托类型的事件了。Order事件。
Order事件,是Customer的成员。因此将事件写在Customer类中。写之前要提升所有类的访问级别:public。因为所有类最后都要配合在一起使用。
首先用委托类型声明一个委托类型的字段,同时不希望被外界访问到。这个委托字段,就是用来存储或者说引用那些事件处理器的。类似Python中a = functionName中的a。
// 事件拥有者的类
public class Customer
{
//声明委托字段
private OrderEventHandler orderEventHandler;
// 声明事件:拿哪个委托类型来约束事件呢?OrderEventHandler。事件名称
public event OrderEventHandler Order
{
// 事件处理器的添加器:
add
{
this.orderEventHandler += value;
}
// 事件处理的移除器
remove
{
this.orderEventHandler -= value;
}
}// 事件声明结束,就可以去使用了。
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}.",this.Bill);
}
}
至此:事件声明完毕,可以直接订阅绑定。硬写事件处理器了。
// 委托类型,声明事件所需。自定义类型的。
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
// 事件拥有者的类
public class Customer
{
//声明委托字段
private OrderEventHandler orderEventHandler;
// 声明事件:拿哪个委托类型来约束事件呢?OrderEventHandler。事件名称
public event OrderEventHandler Order
{
// 事件处理器的添加器:
add
{
this.orderEventHandler += value;
}
// 事件处理的移除器
remove
{
this.orderEventHandler -= value;
}
}// 事件声明结束,就可以去使用了。
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}.",this.Bill);
}
}
// 传递事件消息的类
public class OrderEventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}
第三步:定义事件的响应者:
public class Waiter
{
}
第四步: 订阅事件
static void Main(string[] args)
{
//事件拥有者
Customer customer = new Customer();
// 事件响应者
Waiter waiter = new Waiter();
// 事件、订阅、处理器
customer.Order += waiter.Action;
}
第五步:事件处理器
硬写事件处理器。然后按照智能提示自动补全。
public class Waiter
{
internal void Action(Customer customer, OrderEventArgs e)
{
throw new NotImplementedException();
}
}
可以看到补全的事件处理器中参数列表与我们定义的委托类型的参数是一直的。
完善事件处理器如下:
public class Waiter
{
internal void WaiterHandle(Customer customer, OrderEventArgs e)
{
Console.WriteLine("Serve you the dish-{0}.", e.DishName);
double price = 10;
switch (e.Size)
{
case "Small":
price = price * 0.5;
break;
case "large":
price = price * 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
第六步:触发事件
事件的触发一定由拥有者根据内部逻辑来实现。因此需要为事件拥有者Customer添加一些方法。
// 事件拥有者的类
public class Customer
{
//声明委托字段
private OrderEventHandler orderEventHandler;
// 声明事件:拿哪个委托类型来约束事件呢?OrderEventHandler。事件名称
public event OrderEventHandler Order
{
// 事件处理器的添加器:
add
{
this.orderEventHandler += value;
}
// 事件处理的移除器
remove
{
this.orderEventHandler -= value;
}
}// 事件声明结束,就可以去使用了。
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}.",this.Bill);
}
public void WalkIn()
{
Console.WriteLine("Walk into the Restaurant");
}
public void SitDown()
{
Console.WriteLine("Sit down.");
}
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("let me think...");
Thread.Sleep(500);
}
//触发事件:
// 如果无人订阅这个事件:报错,否则执行。
if (this.orderEventHandler!=null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "kongpao Chicken";
e.Size = "large";
this.orderEventHandler.Invoke(this, e); // 委托的Invoke。
}
}
public void Action()
{
Console.ReadLine();
this.WalkIn();
this.SitDown();
this.Think();
}
}
static void Main(string[] args)
{
//事件拥有者
Customer customer = new Customer();
// 事件响应者
Waiter waiter = new Waiter();
// 事件、订阅、处理器
customer.Order += waiter.WaiterHandle;
//触发事件
customer.Action();
customer.PayTheBill();
}
在事件的触发中,我们用的是委托的Invoke。
而且,在判断事件是否被挂接订阅时,我们用的委托类型的字段。我们能否直接用事件呢?
如果我们改用事件,编译器会提示我们事件只能出现在+=事件订阅器操作器的左边。意思就是说不能去调用其成员。
二、简略声明
//声明委托字段
private OrderEventHandler orderEventHandler;
// 声明事件:拿哪个委托类型来约束事件呢?并且用来存储对事件处理器的引用呢?OrderEventHandler类型。+ 事件名称
public event OrderEventHandler Order
{
// 事件处理器的添加器:
add
{
this.orderEventHandler += value;
}
// 事件处理器的移除器
remove
{
this.orderEventHandler -= value;
}
}// 事件声明结束,就可以去使用了。
完整声明中事件定义的方式如上,而简略声明也叫字段式声明(Feild-like)。看起来像是字段声明。
public event OrderEventHandler Order;
同样的在事件触发中要修改为:
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("let me think...");
Thread.Sleep(500);
}
//触发事件:
// 如果无人订阅这个事件:报错,否则执行。
//if (this.orderEventHandler!=null)
if (this.Order!=null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "kongpao Chicken";
e.Size = "large";
//this.orderEventHandler.Invoke(this, e);
this.Order.Invoke(this, e);
}
}
问题回归到最开始:事件是不是真的变成了一个委托类型的字段?
我们并没有去手动的声明委托类型的字段。对事件处理器的引用到底存储在什么地方。
打开反编译:在Developer Command Prompt for VS2019。输入ildasm。将项目文件夹下bin/Debug下的exe拖入到IL DASM中。查看反编译内容。向下的三角就是事件。
两个水蓝色方块:就是字段。
- k__加两个下滑线:这个是属性Bill简略声明时,编译器为我们准备的后台字段。这就是属性声明背后其实也有字段。
- 另一个字段Order,类型是OrderEventHandler。这个是编译器为我们简略事件声明准备的委托类型字段。所以说委托类型字段还在,后台自动创建,只是没有出现在我们的代码中。
- 这个Order字段可以存储事件处理器的引用。也就是说可以存储方法的引用。
- 这就是隐藏在简略声明事件背后的秘密。
有了委托字段/属性,为什么还需要事件?
- 事件成员能够让程序的逻辑,以及对象之间的关系变得更加的更加“有道理”、更加安全。
- 上面简略声明中,如果把event去掉,那么此时Order就真变成了委托类型的字段。
此时程序正常执行:逻辑没有问题。但是有一个隐患:因为它是public的。
public OrderEventHandler Order;
此时Waiter提供的事件处理器是被Customer的内部逻辑直接调用的。理论上不会有人去给他点餐。但是字段式公开的,有可能在Customer类之外,被修改。
static void Main(string[] args)
{
//事件拥有者
Customer customer = new Customer();
// 事件响应者
Waiter waiter = new Waiter();
// 事件、订阅、处理器
customer.Order += waiter.WaiterHandle;
OrderEventArgs e = new OrderEventArgs();
e.DishName = "ManHanquanxi";
e.Size = "large";
OrderEventArgs e2 = new OrderEventArgs();
e2.DishName = "Beer";
e2.Size = "large";
Customer badGuy = new Customer();
badGuy.Order += waiter.WaiterHandle;
badGuy.Order.Invoke(customer, e);
badGuy.Order.Invoke(customer, e2);
// 点菜的是badGuy,不再是顾客,付钱的确实顾客。字段被篡改。
// 当然此时如果写的时候可以仔细点来避免,但是当程序特别大时,如果没有语言上的限制,这种自由度特别高的应用,很容易滥用误用。
//触发事件
//customer.Action();
customer.PayTheBill();
Console.ReadLine();
}
此时的Order不再是闪电形状的事件了。Order是一个委托类型的字段。
当然此时如果写的时候可以仔细点来避免,但是当程序特别大时,如果没有语言上的限制,这种自由度特别高的应用,很容易滥用误用。
这种情形与C++中的函数指针类似,有时,函数指针莫名指向不确定的函数。一般不出现,但是一旦出现,特别难以Debug。类似的在这就是为何Java中彻底放弃了与函数指针相关的功能——Java没有委托类型。因为指针有时候会莫名指向不想调用的函数上。
这也就是为何微软会推出事件机制,而不再使用委托类型的字段来定义事件。
如果我们把event加上,此时会报错。
public event OrderEventHandler Order;
语言级的限制。
事件的本质:委托字段的一个包装器。
- 这个包装器对委托字段的访问起限制作用,相当于一个"蒙版Mask"。只暴露必要信息。限制了外部对委托字段的访问。使得字段更加安全。
- 这就是面向对象里面一个非常重要的功能:封装(encapsulation)的一个重要功能就是隐藏。
- 事件对外界隐藏了委托实例的大部分功能,仅暴露添加/移除事件处理器的功能。
DotNet自带的声明
除了使用自定义类型的事件委托类型以外,厂商DotNet为我们准备好的通用的专门去声明事件的委托类型。EventHandler类型。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Event06默认方式声明
{
class Program
{
static void Main(string[] args)
{
//事件拥有者
Customer customer = new Customer();
// 事件响应者
Waiter waiter = new Waiter();
//事件、订阅、事件处理器
customer.Order += waiter.Action;
customer.Action();
customer.PayTheBill();
Console.ReadLine();
}
}
public class Customer
{
// 事件声明用DotNet自带的EventHandler类型完成。
public event EventHandler Order;
public double Bill { get; set; }
public void PayTheBill()
{
Console.WriteLine("Customer pay the bill ${0}.",this.Bill);
}
public void Action()
{
if (this.Order != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "Beer";
e.Size = "Large";
this.Order.Invoke(this, e);
}
}
}
// 必须继承于基类
public class OrderEventArgs:EventArgs
{
public String DishName { get; set; }
public string Size { get; set; }
}
public class Waiter
{
internal void Action(object sender, EventArgs e)
{
Customer customer = sender as Customer;
OrderEventArgs orderInfo = e as OrderEventArgs;
Console.WriteLine("Serve the dish-{0} for you!", orderInfo.DishName);
double price = 10;
switch (orderInfo.Size)
{
case "Large":
price = price * 1.5;
break;
case "Small":
price = price * 0.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
定义事件时:直接定义在事件拥有者类内了。而且定义方式是直接用EventHandler声明了,我们安装Ctrl点进去看看定义。
发现其实就是我们自定义声明时一样:都是基于委托类型delegate的。基于委托约束事件处理器方法的参数类型,来保证订阅者的处理器方法匹配。
代码思路:
详情见下一篇文章《C#学习笔记(十七)事件(四)事件个人总结》
事件真的是“以特殊方式声明的委托字段/实例”吗?
- 不是,只是声明的时候“看起来像”(对比委托字段与事件的简化声明,Field-like)
- 事件声明的时候使用了委托类型,简化声明造成事件看起上去像一个委托的字段(实例),而event关键字则更像是一个修饰符——这就是错觉的来源之一。
- 订阅事件的时候+=操作符后面可以是一个委托实例,这与委托实例的赋值方法语法相同,这也让事件看起来像是一个委托字段——这是错觉的又一来源。
- 重申:事件的本质是加装在委托字段上的一个“蒙版”(mask),是个起掩蔽作用的包装器。这个用于阻挡非法操作的“蒙版”绝不是委托字段本身。
事件背后的委托类型字段是由编译器自动生成的(反编译可看到)。编程的时候无法直接访问,为此只能使用事件的名字来判断是否为空,去调用Invoke方法。看起来像字段。但其实,真不是。
为什么要使用委托类型来声明事件?
- 站在source的角度来看,是为了表明source能对外传递哪些消息。
- 站在subscribe的角度来看,它是一种约定,是为了约束能够使用什么样的签名的方法来处理(响应)事件。
- 委托类型的实例将用于存储(引用)事件处理器。
对比事件与属性
- 属性不是字段——很多时候属性是字段的包装器,这个包装器用来保护字段不被滥用。
- 事件不是委托字段——它是委托类型字段的包装器,这个包装器用来保护委托字段不被滥用。
- 包装器永远都不可能是被包装的东西。