1、什么是事件
事件是一种能够使对象或者类提供通知的成员。
类里面有很多成员,比如:属性、方法
事件拥有五个组成部分:
1、事件的拥有者
2、事件成员,也就是事件本身
3、事件的响应者
4、事件处理器
5、事件订阅
2、事件的订阅
注意:一个事件可以挂接多个事件处理器
一个事件处理器可以被多个事件挂接
事件的挂接是通过 " += " 符号进行的
例如: Click += button_Click();
这里就是为Click事件挂接了button_Click()这个事件处理器
但这种方式是一个语法糖,还可以这样写:
Click += new EventHandler(this.button_Click);
这是通过委托的方法挂接事件处理器,EventHandler是c#提供的一种事件处理器委托(不知道委托的可以看我写的委托博客),所以我们常说事件是基于委托驱动的。
我们也可以通过匿名委托挂接事件处理器:
Click += delegate(object sender , EventArgs e){
///处理该事件
}
我们还可以通过匿名函数的方式挂接事件处理器:
Click += (object sender ,EventArgs e)=> {
//处理该事件,这里的“object”和“EventArgs”甚至可以不用写,c#会自己推断他们的数据类型
}
3、自定义事件
3.1 事件的完整声明
c#规定,我们在使用委托来声明事件的时候,委托的名称需要叫做:事件名称+EventHandler,这个事件需要两个参数,一个是事件的拥有者,一个是事件参数,c#规定事件参数需要命名为:事件名称+EventArgs。
如果某个类作为EventArgs来使用,那么它应该继承(也叫派生)自EventArgs这个类。
下面给出一段实例代码,请结合实例代码看我下面的博客:
using System;
using System.Threading;
namespace EventExample
{
class Program
{
static void Main(string[] args)
{
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += waiter.Aciton;
customer.Action();
customer.PayTheBill();
}
}
public class OrderEventArgs: EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}
public delegate void OrderEventHandler (Customer customer, OrderEventArgs e);
public class Customer
{
private 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("I walk into the restaurant");
}
public void SitDown()
{
Console.WriteLine("I sit down");
}
public void think()
{
for(int i = 0; i < 5; i++)
{
Console.WriteLine("Let me think ...");
Thread.Sleep(1000);
}
if(this.orderEventHandler != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "宫保鸡丁";
e.Size = "big";
orderEventHandler.Invoke(this,e);
}
}
public void Action()
{
WalkIn();
SitDown();
think();
}
}
public class Waiter
{
public void Aciton(Customer customer, OrderEventArgs e)
{
Console.WriteLine("I will server you the Dish - {0}",e.DishName);
double price = 10;
switch (e.Size)
{
case "small":
price = price * 0.5;
break;
case "big":
price = price * 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
结果如下:
这个案例模拟了一个人进入餐馆并且坐下点餐然后服务员服务的过程。
我们来看事件的五个组成部分分别是谁:
事件的拥有者: customer 实例
事件成员(事件本事): order 事件
事件的响应者: waiter 实例
事件处理器:Action方法
事件订阅: customer.Order += waiter.Aciton;
这里的事件订阅和委托中的多播委托是不是很像?
这是因为:事件是一种特殊的多播委托,它允许多个方法订阅同一个事件。(这里笔者后面会重点讲)
private OrderEventHandler orderEventHandler; 这个委托字段就是用来存储,或者说是引用事件处理器的
下面的代码就是用来声明事件的,这里
使用event关键字告诉编辑器我现在是在申明一个事件,
使用OrderEventHandler这个委托类型来约束这个事件,
使用add ,remove 分别声明事件处理器的添加器和移除器,
使用 value 这个上下文关键字来获得传入的EventHandler
public event OrderEventHandler Order
{
add
{
this.orderEventHandler += value;
}
remove
{
this.orderEventHandler -= value;
}
}
customer.Order += waiter.Aciton; 这里表示使用waiter的Action方法来作为Order事件的事件处理器
下面这段代码是用来触发事件的,首先判断事件处理器是否为空,也就是该事件是否被订阅(在例子中就是是否有服务员来服务这个顾客),然后通过orderEventHandler.Invoke(this,e);来触发事件。这里肯定有人好奇了orderEventHandler.不是一个事件处理器吗?为什么调用它就是触发事件?
这是因为:事件是基于委托的,事件本质上是一种特殊的多播委托,我们发现在进行事件订阅时,waiter的action方通过事件处理器的添加器被添加到事件处理器的委托链上,当这个事件处理器(也就是多播委托)被执行时,这个委托链上的所有方法也就被执行了,也就相当于事件被触发了,事件对应的事件处理器也被执行了。(所以事件是基于委托驱动的!!)
这里不理解的建议去看一下笔者写的c#委托
if(this.orderEventHandler != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "宫保鸡丁";
e.Size = "big";
orderEventHandler.Invoke(this,e);
}
3.2 事件的简略声明
基于事件完整声明的代码,我们做出如下操作:
删去如下代码:
private OrderEventHandler orderEventHandler;
public event OrderEventHandler Order
{
add
{
this.orderEventHandler += value;
}
remove
{
this.orderEventHandler -= value;
}
}
改为:
public event OrderEventHandler Order;
将代码:
if(this.orderEventHandler != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "宫保鸡丁";
e.Size = "big";
orderEventHandler.Invoke(this,e);
}
改为:
if(this.Order != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "宫保鸡丁";
e.Size = "big";
Order.Invoke(this,e);
}
这里其实应该有问题的,因为这是一种语法糖的写法,按道理来说事件只能出现在 +=和-=操作符的左边,所以这样写在完整写法中是报错的,但是简写中是不报错的,这得怪微软。
我们发现这里我们并没有声明委托类型字段,但是就真的没有吗?
其实通过反编译我们是可以发现编译器给我们准备了一个委托类型字段的,只是在编写代码时我们看不见罢了。
为
了编写规范,我们进行以下更改:
public void think()
{
for(int i = 0; i < 5; i++)
{
Console.WriteLine("Let me think ...");
Thread.Sleep(1000);
}
OnOrder("宫保鸡丁", "big");
}
protected void OnOrder(string dishName ,string size)
{
if(this.Order != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = dishName;
e.Size = size;
Order.Invoke(this, e);
}
}
我们将点餐单独封装成一个方法,并使用protected修饰符保护,防止”借刀杀人“。
4、事件的作用
我们可能会有一个问题:既然有了这个委托类型字段,为什么还需要事件这个成员呢?
其实这是因为,事件可以使得逻辑更加安全,防止“借刀杀人”。
我们可以来看一个例子:
我们在简化的例子中对代码public event OrderEventHandler Order;进行操作,删除event关键字,使得它真的成为一个委托。这时候看上去并不影响程序逻辑,且运行结果也是正确的。
但是我们将Main方法改为如下:
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += waiter.Aciton;
//customer.Action();
OrderEventArgs e1 = new OrderEventArgs();
e1.DishName = "满汉全席";
e1.Size = "big";
OrderEventArgs e2 = new OrderEventArgs();
e2.DishName = "啤酒";
e2.Size = "big";
Customer badGuy = new Customer();
badGuy.Order += waiter.Aciton;
badGuy.Order.Invoke(customer, e1);
badGuy.Order.Invoke(customer, e2);
customer.PayTheBill();
运行结果如下:
这时我们发现,这个badGuy点的菜居然记到了customer头上,它白嫖了!
为什么会出现这种情况呢?本质上就是因为这行代码:
badGuy.Order.Invoke(customer, e1);
badGuy.Order.Invoke(customer, e2);
因为现在的Order是一种委托,所以它可以通过点操作符调用,使得把菜记都别人头上。
如果Order是事件的话,他就只能出现在 += 和 -=操作符左边,这种事情也就不会发送!
所以事件的本质就是委托字段的包装器!对委托起到限制作用!