C#零基础学习之路——事件
一. 学习背景
基于学习b站刘老师的c#入门,整理学习心得。
二. 事件
2.1 什么是事件
能够发生的事情
2.2 事件模型
1.事件的拥有者(类或者对象)
2.事件(事件拥有者的成员)
3.事件的响应者(类或者对象)
4.事件处理器(事件响应者的方法成员)
5.事件订阅:把事件处理器与事件关联起来,本质上是一种以委托类型为基础的约定。
补充:
1.挂接事件处理器的时候,可以使用委托实例,也可以使用方法名,这是"语法糖"。
2.事件处理器对事件的订阅不是随意的,匹配与否由声明事件时所使用的委托类型来检测。
3.事件可以同步调用或者异步调用。
4.事件返回一个委托类型,所有事件处理器(事件响应者的方法)的类型应与委托类型一致。
2.3 简单事件
代码1如下:
using System;
using System.Timers;
namespace EventExampleSample
{
class Program
{
static void Main(string[] args)
{
Timer timer = new Timer();
timer.Interval = 1000;
Boy boy = new Boy();
Girl girl = new Girl();
timer.Elapsed += boy.EventFunction;
timer.Elapsed += girl.EventFunction;
timer.Start();
Console.ReadLine();
}
}
class Boy
{
internal void EventFunction(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Jump!");
}
}
class Girl
{
internal void EventFunction(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Sing!");
}
}
}
代码说明:分析一哈,代码中事件的五部分。第一,事件的拥有者是Timer类的对象timer;第二,事件是timer对象的Elapsed事件;第三,事件的响应者是Boy和Girl类的对象;第四,事件处理器是EventFunction(成员函数);第五,语句(timer.Elapsed += boy.EventFunction;)表示订阅。注意运算符的左边是事件,右边是事件处理器,整条语句代表订阅,不知道声明事件时所使用的委托类型来检测,用编译器智能提示完成对事件处理器的声明。留个疑问:那么这事件是怎么触发的?
当事件的拥有者,同时也是事件的响应者,如代码2:
using System;
using System.Windows.Forms;
namespace EventExampleSample
{
class Program
{
static void Main(string[] args)
{
MForm mForm = new MForm();
mForm.Click += mForm.EventFunction;
mForm.ShowDialog();
}
}
class MForm : Form
{
internal void EventFunction(object sender, EventArgs e)
{
this.Text = DateTime.Now.ToString();
}
}
}
事件处理器参数分析:sender参数,事件的发起者,假如两个按钮的Click事件都关联到同一个事件处理器上,通过判断决定代码逻辑。
三.自定义事件
自定义事件的完整声明如代码3:
using System;
using System.Threading;
namespace EventExample
{
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
class Program
{
static void Main(string[] args)
{
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += waiter.ActionFun;
customer.Action();
customer.PayTheBill();
Console.ReadLine();
}
}
public class OrderEventArgs : EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}
public class Customer
{
public double Bill { get; set; }
private OrderEventHandler orderEventHandler;//该委托字段用于引用事件处理器
public event OrderEventHandler Order
{
add
{
this.orderEventHandler += value;
}
remove
{
this.orderEventHandler -= value;
}
}
public void Think()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
if (this.orderEventHandler != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "Rice";
e.Size = "large";
this.orderEventHandler.Invoke(this, e);//触发事件
}
}
public void Action()
{
Console.ReadLine();
this.Think();
}
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}",this.Bill);
}
}
public class Waiter
{
public void ActionFun(Customer customer, OrderEventArgs e)
{
Console.WriteLine("Dish name {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;
}
}
}
代码说明:按照事件的五大组成部分来分析自定义事件。事件是基于委托的,这句话有两个含义,第一层:事件需要委托类型来作为约束,其规定了事件能发送什么样的消息同时也规定了事件的响应者能收到什么样的消息,也就决定了事件响应者的事件处理器必须能够跟这个约束匹配上,它才能订阅上这个事件。第二层:当事件的响应者提供给事件拥有者相应的事件处理器后,能够记录或引用方法的任务也必须是委托的实例。代码3中,Customer这个类是事件的拥有者,Order是Customer这个类的事件,事件的响应者是Waiter类对象,事件处理器是ActionFun(Waiter类的方法成员),事件订阅(+=)如代码3。那么怎么触发这个
事件的呢?调试程序发现,调用委托字段(orderEventHandler)Invoke时就触发了事件。
解决疑惑:当我们的事件的五大部分都完整时,那这个事件究竟是怎么触发的?比如按钮的点击事件,当我们为一个按钮设置好了一个完整的点击事件后,通过点击按钮我们就触发了这个事件,就会去执行事件处理器。同理在代码3中我们设置好了一个完整的事件后,那什么时候去触发或者说怎么去触发它,那都是我们自己设置的,这里的触发是在Think()函数中等待线程结束Sleep后,触发该事件。也就是说满足触发条件调用相应的事件处理器。
自定义事件的简化声明:
using System;
using System.Threading;
namespace EventExample
{
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
class Program
{
static void Main(string[] args)
{
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += waiter.ActionFun;
customer.Action();
customer.PayTheBill();
Console.ReadLine();
}
}
public class OrderEventArgs : EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}
public class Customer
{
public double Bill { get; set; }
public event OrderEventHandler Order;//看上去像是委托类型的字段,但并不是这样,它是事件
public void Think()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
if (this.Order != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "Rice";
e.Size = "large";
this.Order.Invoke(this, e);//触发事件,这里不得以而为之,因为这是微软的语法糖,在类外是不可以调Invoke方法的
}
}
public void Action()
{
Console.ReadLine();
this.Think();
}
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}",this.Bill);
}
}
public class Waiter
{
public void ActionFun(Customer customer, OrderEventArgs e)
{
Console.WriteLine("Dish name {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;
}
}
}
代码说明:这里简化了自定义事件的声明,不要误认为事件的声明是一个字段,用反编译器查看后,后台为我们生成了一个委托类型的字段,这又是微软的"语法糖"。
有时候,很疑惑,为什么要用事件这个成员,看了代码3后发现,事件好像没啥用,但又说不上来,就调委托就可以了吧。现在的理解是:委托很容易被人滥用,而事件并不是委托的字段而是委托的封装。
使用厂商提供的委托去实现
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.ActionFun;
customer.Action();
customer.PayTheBill();
Console.ReadLine();
}
}
public class OrderEventArgs : EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
}
public class Customer
{
public double Bill { get; set; }
public event EventHandler Order;
public void Think()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Let me think...");
Thread.Sleep(1000);
}
if (this.Order != null)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = "Rice";
e.Size = "large";
this.Order.Invoke(this, e);//触发事件
}
}
public void Action()
{
Console.ReadLine();
this.Think();
}
public void PayTheBill()
{
Console.WriteLine("I will pay ${0}",this.Bill);
}
}
public class Waiter
{
public void ActionFun(object sender, EventArgs e)
{
Customer customer = sender as Customer;
OrderEventArgs eo = e as OrderEventArgs;
Console.WriteLine("Dish name {0}", eo.DishName);
double price = 10;
switch (eo.Size)
{
case "small":
price = price * 0.5;
break;
case "large":
price = price * 1.5;
break;
default:
break;
}
customer.Bill += price;
}
}
}
四.总结
总结:事件的本质是委托字段的包装器,包装器对委托字段的访问起限制作用,事件对外界隐藏了委托实例的大部分功能,仅仅暴露添加和移除事件处理器的功能。