感谢刘铁猛老师的《C#入门详解》和擅码网Monkey老师的《C#面向对象基础》
本专栏的委托与事件部分已经更新完毕,跳转链接如下:
第一篇:感性认识委托
感性认识委托 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/146341073
第二篇:函数指针:委托的由来
函数指针:委托的由来 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/146637091
第三篇:委托的用法
委托的用法 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/147242231
第四篇:感性认识事件
闹钟响了我起床——感性认识事件 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/147932169
第五篇:事件的调用
事件的调用 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/148561855
第六篇:事件的完整声明,触发和事件的本质
事件的完整声明,触发和事件的本质 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/150967817
第七篇:为什么我们需要事件&补充和总结
为什么我们需要事件&补充和总结 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/162065756
第八篇:用委托事件机制模拟游戏场景
浅谈C#委托事件机制:开阔地机枪兵对射问题 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/166465013
每个程序员都会对自己掌握的第一门语言怀有特殊感情,对我来说,这种语言正是C#;希望我的文字能为大家带来一点帮助,还请多多指教~
作为系列的第五篇文章;我们接着上一篇,说说事件的用法。
要熟练使用事件机制,我们先来更深入的了解一下事件模型:
事件模型的五个组成部分:
1>.事件的拥有者(event source,事件的源头,是一个对象或一个类)
皮之不存毛将焉附?事件必须依附于类或对象存在
2>.事件成员(event,事件本身,是对象或类的成员)
事件是一种能让对象或类具备通知能力的成员,从其拥有者角度来看,事件就是一个用于通知别人的工具,事件不会主动去通知别的对象或别的类,只有被其拥有者使事件发生后才会发生
3>.事件响应者(event subscriber,事件的订阅者,是对象或类)
事件的响应者就是事件的订阅者,只有订阅了事件才能进行相应的响应,他们会使用自己的事件处理器来对事件进行应有的响应
4>.事件处理器(event handler,事件处理器,是类或对象的成员)——本质上是一个回调方法
事件的处理器是类或对象的成员,因为方法是类或对象中用于做事情的部分,而处理事件显然也要实现某些逻辑——因此,事件处理器也是一种方法成员,其是回调方法的原因:
事件处理器接收一个委托参数并根据这个参数来进行不同的操作,这些“不同的操作”指不同信息下对应的最终操作,因此是“根据情况选择使用哪个工具”的回调方法
5>.事件订阅:将事件处理器与事件关联在一起,本质上是一种以委托类型为基础的“约定”
事件订阅解决了三个问题:
一,事件发生时是谁应当被通知到(是谁要响应事件)
二,用什么样的方法来响应事件:
小朋友是一个事件,你订阅了小朋友。
当小朋友发出通知“饿了”,你应当做饭;而当小朋友发出通知“迷路”,你应当去找他,你不能在他通知“迷路”时去做饭,这不能解决问题。
为了防止上述情况出现,C#编译器强制规定事件处理器和其关联的事件必须遵循同一个约定;这个约定既约束了事件能发送什么样的消息给事件处理器,也约定了处理器能对什么样的消息进行处理
三,事件的响应者具体用哪一个方法来响应事件
事件的使用包括调用和定义两部分,在大部分人的日常工作中并不会有自定义事件的急迫需求,所以我们先来讨论怎样调用事件。
前面我们已经说过,事件的本质是一种“通知”,事件的调用者在接到这种通知后根据通知信息予以处理的过程就是事件循环。
因此,所谓的“调用”事件,就是给事件所发出的通知“挂载”一个方法,当事件发生时就根据事件参数来调用该方法以完成对事件的处理——这个方法就被称为“事件的处理器”
要给一个事件挂载处理器,我们要用到“+=”操作符,通过“+=”操作符挂载事件处理器到指定事件的行为正是“事件的订阅”
要订阅一个事件,必须先准备一个可以作为事件处理器的方法,前面我们已经说过,事件订阅必须遵循一个以委托为基础的“约定”:你不能用“给小朋友做饭”方法去订阅“小朋友走丢了"事件,因为这在逻辑上毫无意义——现在我们暂时不需要知道这种约定是什么,用编译器的代码提示功能补完代码即可。
为了方便演示,我们using System.Timers以使用.net自带的"计时器计时”事件,要做到这点,我们先声明两个类Boy和Girl以准备为“计时器计时”事件的处理提供实例,然后创建计时器和小朋友的实例。
using System.Timers;
namespace Event
{
class Program
{
static void Main(string[] args)
{
//声明一个每1000毫秒发出一次Elapsed事件的计时器
Timer t1 = new Timer(1000);
Boy b1 = new Boy();
Girl g1 = new Girl();
}
class Boy
{
}
class Girl
{
}
}
}
现在我们尝试为t1的Elaspe事件挂载处理器:
因为我们暂时还没有可以作为处理器的方法,所以我们输入:
t1.Elapsed += b1.Action;
t1.Elapsed += g1.Action;
此时两个Action都会报红(因为还不存在Action方法),使用编译器的CodeSnippet功能生成"符合约定的"Action方法(在VistualStudio中快捷键为Alt+Enter),我们会得到如下方法:(参数列表中的两个参数正是前面提到的“约定”,其中第一个参数代表事件通知的发出者,第二个参数代表详细的通知信息,这两个参数的问题我们下篇文章再讨论)
class Boy
{
internal void Action(object sender, ElapsedEventArgs e)
{
}
}
class Girl
{
internal void Action(object sender, ElapsedEventArgs e)
{
}
}
}
我们为这两个方法填充方法体:
class Boy
{
internal void Action(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Jump!");
}
}
class Girl
{
internal void Action(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Sing");
//输出当前时间是为了方便演示
Console.WriteLine(DateTime.Now);
}
}
}
}
现在,我们已经有了可以作为事件处理器的方法,前面的
t1.Elapsed += b1.Action;
t1.Elapsed += g1.Action;
也已经不再报红了,此时我们已经为t1的Elapsed事件挂载了两个处理器,或者说,男孩b1和女孩g1分别用自己的Action方法订阅了t1的Elaosed事件——现在我们调用t1的Start方法以开始计时事件——这个事件将每隔1000毫秒向其订阅者发出一次通知,程序完整代码为:
using System.Timers;
namespace Event
{
class Program
{
static void Main(string[] args)
{
//这里因为引用了具备同名成员的名称空间,不得不写Timer的全名以防出现不明确的 引用
//声明一个每1000毫秒发出一次Elapsed事件的计时器
Timer t1 = new Timer(1000);
//将该计时器的Elapse事件订阅给两个订阅者
Boy b1 = new Boy();
Girl g1 = new Girl();
t1.Elapsed += b1.Action;
t1.Elapsed += g1.Action;
t1.Start();
//这里加ReadKey是因为不加的话程序就直接返回了
Console.ReadKey();
}
class Boy
{
internal void Action(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Jump!");
}
}
class Girl
{
internal void Action(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Sing");
Console.WriteLine(DateTime.Now);
}
}
}
}
输出为:(每隔一秒输出一次)
Jump!
Sing
2020/6/2 12:15:46
Jump!
Sing
2020/6/2 12:15:47
Jump!
Sing
2020/6/2 12:15:48
Jump!
Sing
2020/6/2 12:15:49
Jump!
Sing
2020/6/2 12:15:50
Jump!
Sing
2020/6/2 12:15:51
Jump!
Sing
2020/6/2 12:15:52
Jump!
Sing
2020/6/2 12:15:53
.......
现在我们来用五步模型分析一下上面这个事件:
事件拥有者:计时器t1
事件:t1的Elapsed事件
事件响应者:男孩b1和女孩g1
事件处理器:男孩和女孩的Action方法
事件订阅关系:b1和g1的Action方法订阅了t1的Elapsed事件
这看似一句废话,但我经过几个月的学习和使用才认识到这个道理……所以还是想饶舌一下,让大家少踩一点坑吧。
如果说声明事件是准备了一把枪,挂载处理器是为这把枪装上了子弹,那调用事件就是扣动扳机了,只有有枪弹结合,扣动扳机才能打出子弹。
一般来说,事件的调用是通过方法进行的,在下篇文章中我们将说明这样的方法得怎样去写。
大家再看上面的代码,会注意到我们通过+=把男孩和女孩的处理器方法挂载到计时器事件后,得调用计时器的Start方法才能真正的启用事件,如果没有这个Start方法,那就是有弹没扣扳机,而如果直接调用Start方法而不给Start方法内部触发的Elapsed事件挂载处理器,那调用同样不会成功(没弹扣扳机)
事件的拥有者和响应者的关系就是事件的订阅关系,其中比较主要的是以下三种,通过这些例子我们可以更好的巩固“事件调用”的知识:
事件拥有者和响应者是完全不同的两个对象,我饿了告诉我妈,我妈替我做饭
using System.Forms;
class Program
{
static void Main(string[] args)
{
Form f1 = new Form();
//这步操作等同于将f1嫁给了c1,从此f1就是c1家的人了
Controller c1 = new Controller(f1);
//f1现在可以使用c1家的东西(Click事件)
f1.ShowDialog();
}
class Controller
{
//每个Controller的实例都包含一个Form类型的字段
private Form form;
//在实例化Controller时,只要参数form不为空就把form赋值给此实例的form字段
public Controller(Form form)
{
if (form != null)
{
this.form = form;
form.Click += ClickForm;
}
}
private void ClickForm(object sender, EventArgs e)
{
this.form.Text = DateTime.Now.ToString();
}
}
}
在本例中,事件的拥有者是c1,事件的响应者是f1,将f1嫁给c1后才能使用c1家的东西,f1和c1是完全独立两个对象
本案例中的事件的元素分析:
事件拥有者:Form类型的实例f1
事件:Form实例的Click事件
事件响应者:Controller类的实例c1
事件处理器:c1的ClickForm方法
事件订阅关系:Controller类的实例c1的事件处理器ClickForm方法订阅着此实例的form字段所指向的实例f1的Click事件
事件拥有者同时也是事件响应者,一个人的眼睛看到了石头然后决定避开石头以免撞倒
using System.Forms;
class Program
{
static void Main(string[] args)
{
Form f1 = new Form();
//这步操作等同于将f1嫁给了c1,从此f1就是c1家的人了
Controller c1 = new Controller(f1);
//f1现在可以使用c1家的东西(Click事件)
f1.ShowDialog();
}
class Controller
{
//每个Controller的实例都包含一个Form类型的字段
private Form form;
//在实例化Controller时,只要参数form不为空就把form赋值给此实例的form字段
public Controller(Form form)
{
if (form != null)
{
this.form = form;
form.Click += ClickForm;
}
}
private void ClickForm(object sender, EventArgs e)
{
this.form.Text = DateTime.Now.ToString();
}
}
}
用自己的方法去订阅和处理自己的事件,这个示例中MyForm类只有一个事件处理器,但实际工作中可能有很多很多的事件处理器待选择,必须选出那些能正确实现逻辑的处理器
本案例中的事件的元素分析:
事件拥有者:MyForm类的实例f1
事件:f1的Click事件
事件响应者:MyForm的实例f1
事件处理器:Myform类的ClickForm方法
事件订阅关系:MyForm类的实例f1的ClickForm方法订阅着MyForm类实例f1的Click事件
某个实例用自己的方法去订阅自己成员的事件
class Program
{
static void Main(string[] args)
{
MyForm form1 = new MyForm();
form1.ShowDialog();
}
//定义一个继承自巨硬Form类的MyForm类
class MyForm : Form
{
//规定MyForm实例拥有的两个字段
private Button button1;
private TextBox text1;
//MyForm的初始化器
public MyForm()
{
//为字段赋初始值
this.button1 = new Button();
this.text1 = new TextBox();
//使按钮和文本框显示出来
this.Controls.Add(button1);
this.Controls.Add(text1);
//调整文本框位置
button1.Top = 30;
//调整按钮文字
button1.Text = "Say Hello!";
//为按钮的点击事件挂载处理器
button1.Click += ClickSayHello;
}
//按钮的点击事件处理器
private void ClickSayHello(object sender, EventArgs e)
{
this.text1.Text = "Hello World!";
}
}
}
输出为:
本案例中的事件的元素分析:
事件拥有者:f1的字段成员button1
事件:button1的Click事件
事件响应者:f1
事件处理器:f1的ClickSayHello方法
事件订阅关系:f1的ClickSayHello方法订阅着f1成员button1的Click事件
在上面的例子中多添加一个Button,并通过对Sender的判断来进行不同的响应:
class Program
{
static void Main(string[] args)
{
MyForm form1 = new MyForm();
form1.ShowDialog();
}
//定义一个继承自巨硬Form类的MyForm类
class MyForm : Form
{
//规定MyForm实例拥有的两个字段
private Button button1;
private Button button2;
private TextBox text1;
//MyForm的初始化器
public MyForm()
{
//为字段赋初始值
this.button1 = new Button();
this.button2 = new Button();
this.text1 = new TextBox();
//使按钮和文本框显示出来
this.Controls.Add(button1);
this.Controls.Add(button2);
this.Controls.Add(text1);
//调整按钮位置
button1.Top = 30;
button2.Top = 60;
//调整按钮文字
button1.Text = "Boy";
button2.Text = "Girl";
//为按钮的点击事件挂载处理器
button1.Click += ClickButton;
button2.Click += ClickButton;
}
//按钮的点击事件处理器
private void ClickButton(object sender, EventArgs e)
{
if (sender == button1)
{
text1.Text = "我是男孩!";
}
else
{
text1.Text = "我是女孩!";
}
}
}
}
输出为:
其实今天的主题只有一个:怎样调用事件
要调用事件,我们就要先准备一个符合事件约定的方法作为处理器,然后将事件挂载到该处理器上,至于这种“约定”到底是什么,我们会在下篇文章中进行更进一步的学习~