1. 简介
-
事件是一种类型安全的委托(具体实例说明见《精通C#》 --10.5 C#事件)
为什么这么说呢?可以类比属性和字段,属性是对字段的封装,其实 事件也就是封装了一个委托。但是你要知道:委托是一种自定义的数据类型,事件只是一种类的成员。
-
那么现在你要问:事件到底有什么具体的作用?
事件呢?就是有一段代码(方法)触发(raise)事件,事件被触发后则订阅事件的事件处理程序(event handler) 就会开始执行。
从这点看,事件类似异常,因为他们都是由对象引发的,并且可以由我们提供的代码处理
-
订阅一个事件的含义:是提供代码在事件发生时执行这些代码,这些代码称为事件处理程序
订阅事件的语法如下:
事件名+=new 委托名(方法名);
事件名+=函数名注意订阅事件的可以是委托对象,也可以直接是一个函数或是匿名函数或Lambda表达式
只要这个函数的返回值类型和签名与事件一致即可
-
我们都知道委托是类似c语言中的函数针,是一种函数指针在面向对象中的封装,是一种函数回调机制(回调函数就是一通过函数指针调用函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的数时,我们就说这是回调函数。);
事件是用户与应用程序交互的基础,它是回调机制的一种应用。
简而言之:委托的本质是引用类型,用包装回调函数,用于实现回调机制。事件的本质是一个类型安全的(多播)委托,事件是回调机制的一种应用。
-
事件和委托的关系
事件的委托类似属性和字段,事件是对委托的封装,这句话意思就是:事件是一类型安全的委托
事件比委托有更多的限制
1、事件只能同“+=”和“-=”来绑定方(在事件中这个方法叫事件的处理程序,其实这这种绑定方法就是多播委托的绑定方法
2、只能在类的内部调用(触发)事件,但是委托就可以在类外调用函数那样调用
2.实际案例
2.1实际案例1
代码实际背景:
当裁判的发令枪响起,触发事件,事件触发执行的动作就是运动员跑起来了
裁判是发布者,触发事件的方法是发令枪响,
运动员是事件的订阅者,事件发生后就开跑。
class Program
{
static void Main(string[] args)
{
//实例化事件发布者
Judgment judgment = new Judgment();
//实例化事件订阅者
RunSporters runsporter = new RunSporters();
// 订阅者类RunSporters中的方法Run()和RunFail()订阅事件eventRun
//通过委托订阅
judgment.eventRun += new Judgment.delegateRun(runsporter.Run);
//通过函数订阅
judgment.eventRun += runsporter.RunFail;
//执行引发事件的方法,这句代码之后事件的订阅者中的事件处理程序Run()和RunFail()开始运行
judgment.Begin();
Console.ReadKey();
}
}
//事件发布者,也称发布器(publisher)
//发布器(publisher) 是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。
//发布器(publisher)类的对象调用这个事件,并通知其他的对象。
class Judgment
{
//定义一个委托(你要明白:这个委托类型就是事件处理程序的函数类型)
public delegate void delegateRun();
//定义一个事件
//事件的声明与之前委托变量delegate1的声明唯一的区别是多了一个event关键字,且委托写“()”,事件不需要
public event delegateRun eventRun;
//引发事件的方法
//当方法Begin()被执行了,此时就会触发事件 eventRun
public void Begin()
{
if (eventRun != null)
{
//被引发的事件
eventRun();
//☆☆☆☆☆☆☆☆☆☆☆☆
//注意这里就是体现“事件只能在类的内部调用”的地方,你在这个发布者类之外不能调用事件eventRun
}
}
}
//事件订阅者,也称订阅器(subscriber)
//订阅器(subscriber) 是一个接受事件并提供事件处理程序的对象。
//在发布器(publisher)类中的委托调用订阅器(subscriber)类中的方法(事件处理程序)。
//一个事件可以有多个订阅者,事件的发布者也可以是事件的订阅者。
public class RunSporters
{
//事件处理程序
public void Run()
{
Console.WriteLine("运动员跑起来了");
}
//事件处理程序
public void RunFail()
{
Console.WriteLine("有一个起跑失败");
}
}
2.2实际案例2--带有参数的事件
代码实际背景:
热水器仅仅负责烧水,有一个温度字段temperature ,它不能发出警也不能显示水温;
在水烧开时由警报器发出警报;
显示器显示提示和水温。
观察者模式:
Subject:监视对象,它往往包含着其他对象所感兴趣的内容。
在本范例中,热水器就是一个监视对象,它包含的其他对象所感兴趣的内容,就是temprature字段,当这个字段的值快到100时,会不断把数据发给监视它的对象。
Observer:监视者,它监视Subject,当Subject中的某件事发生的时候,会告知Observer,而Observer则会采取相应的行动。
在本范例中,Observer有警报器和显示器,它们采取的行动分别是发出警报和显示水温。
在本例中,事情发生的顺序应该是这样的:
警报器和显示器告诉热水器,它对它的温度比较感兴趣(注册)。
热水器知道后保留对警报器和显示器的引用。
热水器进行烧水这一动作,当水温超过95度时,通过对警报器和显示器的引用,自动调用警报器的MakeAlert()方法、显示器的ShowMsg()方法。
//发布者--热水器
public class Heater
{
private int temperature;
public delegate void BoilHandler(int param); //声明委托
public event BoilHandler BoilEvent; //声明事件
// 烧水
public void BoilWater()
{
for (int i = 0; i <= 100; i++)
{
temperature = i;
//当温度大于95度开始触发事件
if (temperature > 95)
{
//如果有对象注册
if (BoilEvent != null)
{
BoilEvent(temperature); //调用所有注册对象的方法(事件处理程序)
}
}
}
}
}
//订阅者--显示器
public class Display
{
//静态方法
public static void ShowMsg(int param)
{
Console.WriteLine("Display:水快烧开了,当前温度:{0}度。
", param);
}
}
// 订阅器--- 警报器
public class Alarm
{
public void MakeAlert(int param)
{
Console.WriteLine("Alarm:嘀嘀嘀,水已经{0} 度了:", param);
}
}
class Program
{
static void Main(string[] args)
{
//注意我们不需要实例化Display类,为什么呢?因为我们只是使用它里面的一个静态方法,直接使用类名点
Heater heater = new Heater();
Alarm alarm = new Alarm();
//注册方法(订阅事件)
//法1
heater.BoilEvent += new Heater.BoilHandler(alarm.MakeAlert);
//法2
heater.BoilEvent += (new Alarm()).MakeAlert; //给匿名对象注册方法
heater.BoilEvent += Display.ShowMsg; //注册静态方法,静态类中的方法调用直接使用类名点
heater.BoilWater(); //触发事件,会自动调用注册过对象的方法
Console.ReadKey();
}
}
3.标准事件的用法
C#的BCL(基础类库)中针对事件定义的时候所需要的委托有一个标准委托
public delegate void EventHandler(Object sender, EventArgs e);
其中:
-
第一个参数sender 用来保存触发事件的对象的引用。由于是Object类型的,所以可以匹配任何类型的实例。
-
第二个参数用来保存有该事件相关的信息
3.1通过扩展EventArgs来传递数据
注意到Eventhandler的第二个参数是EventArgs类型的,
那么我们来看看EventArgs类
public class EventArgs
{
public static readonly EventArgs Empty;
public EventArgs();
}
EventArgs被设计为不能传递任何数据,如果你想要传递数据怎么办?
我们可以通过扩展EventArgs来传递数据:也就是声明一个EventArgs的子类,使用合适的字段来保存需要传递的数据
比如,,我们要传递一个int类型的参数:
public class ExtendEventArgs:EventArgs
{
public int Tem;
public ExtendEventArgs(int tem)
{
Tem=tem;
}
}
那么这时候我们就可以这样声明EventHandler委托
public delegate void EventHandler(Object sender,ExtendEventArgs e);
你要知道的是C#2.0中引入了EventHandler泛型委托:
所以你不需要因为修改参数而重新声明EventHandler委托
你直接这样定义事件:
public event EventHandler<ExtendEventArgs> MyEvent;
3.2代码实例
代码背景:
汽车销售类CarDealer和顾客类Consumer
CarDealer提供一个新车到达出发事件,Consumer类订阅该事件
class Program
{
static void Main(string[] args)
{
CarDealer dealer = new CarDealer();
Consumer consumer=new Consumer ("志铭");
dealer.NewCarEvent += consumer.ConsumerReply;
dealer.RaiseNewCarInfo("BMW");
Console.ReadKey();
}
//扩展EventArgs类,添加一个Car属性用于传递数据
public class CarInfoEventArgs : EventArgs
{
public CarInfoEventArgs(string car)
{
this.Car = car;
}
public string Car;
}
//发布类
public class CarDealer
{
//public delegate EventHandler(object sender, CarInfoEventArgs e);//声明委托EventHandler
//public event EventHandler NewCarEvent;//声明事件NewCarEvent
//下面一行就可以代替上面注释的两行
//声明事件NewCarIfno
public event EventHandler<CarInfoEventArgs> NewCarEvent;
//触发事件NewCarIfno的函数
public void RaiseNewCarInfo(string car)
{
Console.WriteLine($"CarDealer :new car {car}");
if (NewCarEvent != null)
{
NewCarEvent(this, new CarInfoEventArgs(car));
//注意:事件的参数类型,声明事件时所用的委托的参数类型和事件处理程序的参数类型三者一样
//注意这里的参数this
//你想想为什么写this?
//EventHandler委托的两个参数,第一个参数就是触发事件的事件对象
}
}
}
//订阅类
public class Consumer
{
public string Name;
public Consumer (string name)
{
this.Name = name;
}
//事件处理程序,注意参数和事件的委托EventHandler<CarInfoEventArgs>一样
public void ConsumerReply(object sender, CarInfoEventArgs e)
{
Console.WriteLine($"{this.Name }:car {e.Car } very good!");
}
}
}
运行结果
CarDealer:new car BMW
志铭:car BMW very good!