深入浅出话事件
小序
在上篇文章(《深入浅出话委托》)中,我们集中讨论了什么是委托以及委托的用法。有朋友问:什么时候用委托——说实话,使用某种编程要素是一种思想,更是一种习惯。举个极端点的例子:比如你问我“什么时候使用for循环”,我完全可以回答——根本用不着for循环,用if加goto就完全能够搞定——我们大多数人使用for循环,是因为我们认同for循环的思想,并且养成了使用for循环的习惯。委托也是这样——没有委托的日子,程序员们一样在干活,只是有了委托机制后,大家干起来更方便、写出的代码质量更高——当你体验到它的方便、自然而然地使用它、养成一种习惯后,你就知道什么时候应该使用它了。OK,我们回到正题上来,委托最常用的一个领域是用来声明“事件”,或者说:委托是事件的基础。作为《深入浅出话委托》的姊妹篇,本文我们主要来讨论事件(Event)。
正文
一.什么是事件
程序语言是对人类语言的抽象,因此,程序语言要素往往与人类语言中对应的词汇有着相近的含义。正是这种“相近”,让很多要素看上去很好懂,但如果不仔细理解,就会误入歧途。“事件”就是这类要素中的一个,让我们小心对待。
现实世界中的事件
先让我们看一看现实世界中的“事件”。
现实世界中的“事件”(Event)是指“有一定社会意义或影响的大事情”。 提取一下句子的二级主干,我们可以得出:事件=有意义或影响的事情。因此,我们可以看出,判定是否为一个“事件”有两个先决条件:首先它要是一个“事情”,然后这个事情还要“有意义或影响”。
接着,我们进一步分析“事情”这个概念。我们常说“一件事情发生了”,这个“发生”组成要素又无外乎时间、地点、参与人物(主体)、所涉及的客体——抽象一点,我们可以把这些要素称为“事情”的参数。
一件事情发生了,可能对某些客体(Client)产生影响,也可能没有任何影响。如果事情发生了、并对客体产生了影响,这时候,我们就应该拿出影响这一影响的办法来。
举个例子:大楼的火警响了(火警鸣响这一事件发生),它产生的影响是让楼内的所有人员都听到了警报声,楼内的人纷纷拿出自己响应这一影响的方法来——普通职员们飞奔出大楼,而消防人员却向相反的方向跑,冲向最危险的火场。我们把这种套路称为“事件响应机制”,用于响应事件所造成的影响而采取的行动简称为“事件响应方法”。特别注意:员工逃跑和消防员冲向火场都是对警号鸣响这一事件的响应方法,而非事件所产生的影响。
对了,还有个小问题:火警响了,我们为什么会跑呢?呵呵,答案很简单——因为我们时刻关心着警报会不会响这个事件。
OK,非常感谢你能把上面的文字读完——初中语文老师的水平完全可以决定一个学生以后是不是能成为一名合格的程序员。
.NET Framework 中事件的概念
下面让我们再来看看C#中“事件”(Event)是什么,并且是如何与现实世界中的事件概念相对应。
1. MSDN对event关键字的解释:
Events are used on classes and structs to notify objects of occurrences that may affect their state.
事件被用在类和结构体上,用处是通知某些对象——这些对象的状态有可能被事件的发生所影响。
2. MSDN对Event的解释:
An event is a way for a class to provide notifications when something of interest happens.
事件,是当某些被关注的事情发生时类提供通知的一种途径。
3. C# Spec中对Event成员的解释:
An event is a member that enables an object or class to provide notifications. Clients can attach executable code for events by supplying event handlers.
事件是一种类成员,它使得对象或类能够提供通知。客户端(被通知的对象/类)可以为事件附加上一些可执行代码来响应事件,这些可执行代码称为“事件处理器”(Event Handlers)。
4. 我自己的解释:
Events is a kind of member of class and structs, and it is a way for class and structs who own the events to notify objects who cares these events and provides the event handlers when these events fire.
事件是类和结构体的一种成员。当事件发生时,“事件”是一种拥有此事件的类/结构体通知关心(或者称为“订阅”)这些事件、并提供事件处理器的对象的一种途径。
干说没啥意思,下面我还是给出相应的代码,带领大家体验一下什么是事件、事件是怎么声明的、事件如何被其它类“订阅”、订阅了事件的类又是如何响应(处理)事件的。
我们就以大楼火警为例,给出下面的代码:
//=============水之真谛============
//
// http://blog.csdn.net/FantasiaX
//
//=========上善若水,润物无声==========
using System;
using System.Collections.Generic;
using System.Text;
namespace EventSample
{
// 委托是事件的基础,是通知的发送方与接收方双方共同遵守的"约定"
delegate void FireAlarmDelegate();
// 大楼(类)
class Building
{
// 声明事件:事件以委托为基础
public event FireAlarmDelegate FireAlarmRing;
//大楼失火,引发火警鸣响事件
public void OnFire()
{
this.FireAlarmRing();
}
}
// 员工(类)
class Employee
{
// 这是员工对火警事件的响应,即员工的Event handler。注意与委托的匹配。
public void RunAway()
{
Console.WriteLine("Running awary...");
}
}
// 消防员(类)
class Fireman
{
// 这是消防员对火警事件的响应,即消防员的Event handler。注意与委托的匹配。
public void RushIntoFire()
{
Console.WriteLine("Fighting with fire...");
}
}
class Program
{
static void
Main
(string[] args)
{
Building sigma = new Building();
Employee employee = new Employee();
Fireman fireman = new Fireman();
// 事件的影响者"订阅"事件,开始关心这个事件发生没发生
sigma.FireAlarmRing+=new FireAlarmDelegate(employee.RunAway);
sigma.FireAlarmRing += new FireAlarmDelegate(fireman.RushIntoFire);
//由你来放火!
Console.WriteLine("Please input 'FIRE' to fire the building...");
string str = Console.ReadLine();
if (str.ToUpper()=="FIRE")
{
sigma. OnFire();
}
}
}
}
上面的代码中提到:事件是基于委托的,委托不但是声明事件的基础,同时也是通知收发双方必需共同遵守的一个“约定”。OK,让我们改进一下上面的例子,进一步发挥事件的威力。
设想这样一个情况:大楼一共是7层,每层的防火做的也不错,只要不是火特别大那么就没必要让所有人都撤离——哪层着火,哪层员工撤离。还有就是一个火警的级别问题:我们把火的大小分为三级——
C级(小火):打火机级,我左边的兄弟比较喜欢抽烟,一般他点烟的时候我不跑。
B级(中火):比较大了,要求所在楼层的人员撤离。
A级(大火):一般女友发脾气都是这个级别,要求全楼人撤离。
OK,让我们看看代码:
//=============水之真谛============
//
// http://blog.csdn.net/FantasiaX
//
//=========上善若水,润物无声==========
using System;
using System.Collections.Generic;
using System.Text;
namespace EventSample
{
// 事件参数类:记载着火的楼层和级别
class FireEventArgs
{
public int floor;
public char fireLevel;
}
// 委托是事件的基础,是通知的发送方与接收方共同遵守的"约定"
delegate void FireAlarmDelegate(object sender, FireEventArgs e);
// 大楼(类)
class Building
{
// 声明事件:事件以委托为基础
public event FireAlarmDelegate FireAlarmRing;
//大楼失火,引发火警鸣响事件
public void OnFire(int floor, char level)
{
FireEventArgs e = new FireEventArgs();
e.floor = floor;
e.fireLevel = level;
this.FireAlarmRing(this, e);
}
public string buildingName;
}
// 员工(类)
class Employee
{
public string workingPlace;
public int workingFloor;
// 这是员工对火警事件的响应,即员工的Event handler。注意与委托的匹配。
public void RunAway(object sender, FireEventArgs e)
{
Building firePlace = (Building)sender;
if (firePlace.buildingName == this.workingPlace && (e.fireLevel == 'A' || e.floor == this.workingFloor))
{
Console.WriteLine("Running awary...");
}
}
}
// 消防员(类)
class Fireman
{
// 这是消防员对火警事件的响应,即消防员的Event handler。注意与委托的匹配。
public void RushIntoFire(object sender, FireEventArgs e)
{
Console.WriteLine("Fighting with fire...");
}
}
class Program
{
static void
Main
(string[] args)
{
Building sigma = new Building();
Employee employee = new Employee();
Fireman fireman = new Fireman();
sigma.buildingName = "Sigma";
employee.workingPlace = "Sigma";
employee.workingFloor = 1;
// 事件的影响者"订阅"事件,开始关心这个事件发生没发生
sigma.FireAlarmRing += new FireAlarmDelegate(employee.RunAway);
sigma.FireAlarmRing += new FireAlarmDelegate(fireman.RushIntoFire);
//由你来放火!
Console.WriteLine("Please input 'FIRE' to fire the building...");
string str = Console.ReadLine();
if (str.ToUpper() == "FIRE")
{
sigma.OnFire(7, 'C');
}
}
}
}
我们仔细分析一下上面的代码:
1. 较之第一个例子,本例中多了一个class FireEventArgs 类,这个类是专门用于传递“着火”这件事的参数的——请回过头去看“现实世界中的事件”一段——我们关心的主要是着火的地点和火的级别,因为在这个类中我们有两个成员变量。
2. 接下来的委托也有所改变,由无参变成了两个参数——object sender, FireEventArgs e ,大家可能会问:为什么你写两个参数,而不用3个或者4个?唔……传统习惯就是这样,这个传统的开端应该可以追溯到VC的Win32时代吧——那时候,消息传递的参数就是一个lParam和一个wParam,分别承载着各种有用的信息——VB模仿它们,一个改名叫sender,一个改名叫e,然后C#又传承了VB,就有了你看到的样子。至于为什么第一个参数是object型,解释起来需要用到一些“多态”的知识,在这里我先不说了,我会在《深入浅出话多态》中以之为例。第二个参数使用了我们自己制造的类,这个类里带着两个有用的信息,上面也提到过了。
3. 在Building类的OnFire方法函数中,进行了参数的传递——你完全可以理解成:Building类将这些参数(一个是this,也就是自己,另一个是e,承载着重要信息)发送给了接收者,也就是发送给了关心/订阅了这一事件的类。在我们这个例子中,订阅了Building类事件的对象分别是employee和fireman,他们会在事件发生的时候得到通知,并从传递给他们的事件参数中筛选自己关心的内容。这一筛选是由程序员完成的,在本例中,Employee对消息就做了筛选,而Fireman类不加筛选,见火就灭(我有点担心我左边的兄弟)。
4. 注意:Building类有一个buildingName域,因为Building是事件的持有者,所以在事件激发的时候,它也是消息的发送者(sender),所以,这个buildingName域也会随着this被发送出去。如果你理解什么是多态,那么OK,你会明白为什么可以使用Building firePlace = (Building)sender;把这个buildingName读出来。
5. Employee类是本程序中最有意思的类。它不但提供了对Build类事件的影响,还会对事件进行智能筛选——只有当自己所工作的大厦警报鸣响,并且是自己所在楼层失火或火势足够大时才会撤离。请仔细分析public void RunAway(object sender, FireEventArgs e)方法。
6. 与Employee类不同,Fireman类对事件是不加筛选的——你想啦,灭火可是消防员的职责,不论哪里着火,他们都会勇往直前!
7. 进入主程序,代码相当清晰——的确是这样,基本类库的代码总是比较复杂(在本例中,基本类库是指Building、Employee、Fireman这几个类)。一般情况下,基本类库与主程序的开发者不是一个人,基本类库的源代码一般也不向主程序的开发者开放,而是以DLL(Assembly)文件的形式发放,所以实际开发中整个程序看起来是非常清晰的。
8. sigma.FireAlarmRing += new FireAlarmDelegate(employee.RunAway);
sigma.FireAlarmRing += new FireAlarmDelegate(fireman.RushIntoFire);
通过这两句你能看出什么来?呵呵,因为事件是基于委托的,所以事件也是多播的!如果不明白什么是多播委托,请参见《深入浅出话委托》。
9. 因为员工在 1F 工作,所以7层的C级火不会导致其撤离——只有消防员会响应。
二.事件的由来
在传统的面向对象的概念中是没有“事件”这个概念的。传统的面向对象概念中只有数据(Data,也称为field、域、成员变量)和方法(Method,也就是成员函数、function)。如果我没记错,那么事件这个概念最早出现在微软的COM技术中,又因为VB是基于ActiveX(COM的一种)的,所以“事件”这一概念便通过VB广而推之、为众多程序员所熟知并使用的——我就是其中的一员。
.NET Framework实际上是对COM的更高层级的封装——要知道,早先.NET Framework这个名字没有出来之前,它叫“COM 3 ” 来着——自然就保留了对事件的支持。
三.事件的意义
《进化论》说:“物竞天择,合理即存在。”
微软说:“我是老大,存在即合理!”
姑且不管微软是不是在耍大牌、搞霸权——事件的存在的确给程序的开发带来了很多方便。从设计层面上讲,它使程序在逻辑思维方面变得简洁清晰、便于维护;从技术层面上讲,它把坚涩难懂的Windows消息传递机制包装的漂漂亮亮,极大地降低了程序员入职的门槛儿。
从软件工程的角度上来看,事件是一种通知机制,是类与类之间保持同步的途径。
问曰:什么同步?
答曰:消息同步!
四.事件的本质——对消息传递的封装
事件可以被激发(Fire,也有称为“引发”的),一个类所包含的成员事件可以在多种情况下被激发。最典型的:一个按钮的Click事件,可以由用户使用鼠标来激发它,也可以由测试这个软件的另一个软件通过Win32 API函数来激发它。
我们来简要讨论一下这个Click事件:
其实,如果你了解Win32的本质,你应该明白用户是不可能直接接触到某个控件的。表面上看,的确是用户用鼠标点击了一下按钮。而实际上,当用户按下鼠标左键的时候是通过鼠标向Windows操作系统发送了一个“左键单击[x,y]点”消息,然后Windows再根据[x,y]的位置把这个消息分配(路由)给应该接收它的控件——这就是Windows的消息传递/路由机制。
同理,当你移动鼠标的时候,看似好像指针在随你的意愿移动,而实际上是你的鼠标在以每秒钟几百次的频率把当前位置汇报给Windows操作系统,然后Windows再把一个漂亮的指针“画”在屏幕上给你看——哈哈,我们都被骗了!
然而这些内容对于C#程序员都是不可见的——都被封装成了“事件”。因此,从Windows系统的机理上讲,事件机制就是对Windows消息传递机制的包装。
下面的代码是对Visual Studio 2005自动生成的WinForm程序的一个模拟。读懂之后,大家可以自己写一个WinForm,对照剖析其中的机理。
代码:
//============水之真谛============//
// //
// http://blog.csdn.net/FantasiaX //
// //
//========上善若水,润物无声=========//
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms; //先添加对System.Windows.Forms和System.Drawing程序集的引用哦!
using System.Drawing;
namespace EmulateWinForm
{
// 自定义的EmulateForm类,派生自Form类。
class EmulateForm : Form
{
//两个控件
private Button myButton;
private TextBox myTextBox;
//初始化各个控件和窗体本身,并把控件加入窗体的Controls数组。
private void InitializeComponent()
{
myButton = new Button();
myTextBox = new TextBox();
myButton.Location = new System.Drawing.Point(195, 38);
myButton.Size = new System.Drawing.Size(75, 23);
myButton.Text = "Click Me";
myButton.Click += new EventHandler(myButton_Click);//挂接事件处理函数
myTextBox.Location = new System.Drawing.Point(12, 12);
myTextBox.Size = new System.Drawing.Size(258, 20);
Controls.Add(myButton);
Controls.Add(myTextBox);
Text = "EmulateForm";
}
//myButton的Click事件发生时,EmulateForm类给予的事件响应函数(Event Handler)
void myButton_Click(object sender, EventArgs e)
{
myTextBox.Text = "Hello, Event World!";
}
//在EmulateForm类的构造函数中执行上面的初始化方法
public EmulateForm()
{
InitializeComponent();
}
}
class Program
{
static void
Main
(string[] args)
{
EmulateForm myForm = new EmulateForm();
Application.Run(myForm);
}
}
}
代码剖析:
1. 要想引用using System.Drawing; using System.Windows.Forms;这两个Namespace,首先要手动添加对System.Drawing和System.Windows.Forms两个程序集(Assembly)的引用。
2. EmulateForm类是自定义的,注意,它派生自Form类。为了清晰起见,我已经把代码简化到了几乎最简……只有两个成员变量。myButton是Button类的一个实例;myTextBox是TextBox类的一个实例。EmulateForm类的成员方法private void InitializeComponent()完全是对真正WinForm程序的模仿,在它的函数体里,对成员变量进行了实例化、初始化(比如确定大小和位置),并将它们加入了窗体的Controls数组里。这个函数将在EmulateForm的构造函数里被执行。
3. 本例中最重要的部分就是对myButton的初始化。注意这句:myButton.Click += new EventHandler(myButton_Click);
myButton.Click是myButton的Click事件,你可能会奇怪:这回怎么不用自己去声明一个事件了呢?呵呵,因为.NET Framework已经为我们准备好了这个事件,你直接用就好了。不过,我们是为了探寻底细而来,所以我还得仔细说一说这个事件。
4. 详细剖析Button.Click事件:首先,事件都是基于委托的,那么myButton.Click事件是基于哪个委托呢?通过查找MSDN,你可以发现myButton.Click是继承自Control类,并基于EventHandler这一委托——下面是EventHandler委托的声明
[SerializableAttribute]
[ComVisibleAttribute(true)]
public delegate void EventHandler (Object sender, EventArgs e)
如果你不太了解事件是怎么声明的,回过头去温习一下《深入浅出话事件(上)》。
在这个声明中,方括号中的是Attribute,你暂时不用去理会它。关键是看EventHandler这个委托:这个委托的参数列表要求它所挂接的函数(对于事件来说就是挂接的事件处理函数)应该具有两个参数——Object类型的sender和EventArgs类型的e。这两个参数起什么作用呢?呵呵,其实非常好玩儿——前面说过了,事件机制是对消息机制的封装,你可以把消息理解成一枚炮弹,sender就是“谁发射的炮弹”,e就是“炮弹里装的什么东西”,炮弹的目标当然就是消息的接收处理者了。我们仔细回顾一下上篇亲手写的那个FireEventArgs类:这个类里不是有两个成员变量吗?一个是代表着火楼层的floor,一个是代表火级的fireLevel,随着Building类实例的FireAlarmRing事件引发,FireEventArgs类的实例e就被发射到了Employee类和Fireman类的实例那里,这两个实例再打开“炮弹”根据发射过来的内容给出相应处理。就像真实的战争中的炮弹有常规弹、穿甲弹、燃烧弹等等一样,我们的“消息炮弹”也不只一种,信手拈来几个与大家共赏一下:
① EventArgs类:这个就是Click事件中使用的那个。算是常规弹吧。因为用户点击按钮是个非常简单的事件,不需要它携带更多的信息了。
② MouseEventArgs类:是由MouseMove、MouseUp、MouseDown事件发射出来。它的实例携带了很多其它的信息,其中最常用的就是一个X和一个Y——用腿肚子想也能想明白,那是鼠标当前的位置。后面的例子中我们给出演示。
③ PaintEventArg类:由Paint事件发送出来。这颗炮弹可不简单,那些非常漂亮的自定义控件都离不开它!在它的肚子里携带有一个Graphics,代表的是你可以在上面绘画的一块“画布”……
OK,先列举3个吧MSDN里有它们的全家福,位置是System.EventArgs的Derived Classes树。微软在.NET Framework方面可谓下足了功夫,从这些Event Args(事件参数),到各种委托,再到五花八门的事件,都已经为我们做了良好的封装,我们只需要拿出来用就是了。
5. void myButton_Click(object sender, EventArgs e)是EmulateForm类对myButton.Click事件的响应函数(也称事件处理器,Eventhandler)。注意它的参数列表,是不是与EventHandler委托一致啊:p
6. 主程序没什么好说的了——new一个EmulateForm的实例出来,用Application.Run方法执行程序就好了。
7. 顺便在这里做一个纠偏:上面已经解释过sender是什么了——它是消息的发送者。我屡次在一些书中发现诸如“事件发送者”这类的话,这是不对的!你想啊,事件只能引发、激发、发生,怎么可能“发送”呢?不合逻辑……
作业1:
建立一个WinForm程序,如图。包含1个Panel,3个TextBox,1个Button。
要求:
1. 当鼠标在Panel里滑动时,textBox1和textBox2分别显示鼠标当前的X和Y。
2. 当鼠标点击按钮时,textBox3要显示Hello Events World!字样。
提示:
1. 留心MouseMove事件的e
2. 留心Visual Studio 2005使用的是C# 2.0,并且使用partial关键字将Form1类的代码分别存储在了Form1.cs和Form1.Designer.cs两个文件里。
作业2:
将《深入浅出话事件(上)》中嘎子炸鬼子的程序升级至使用事件的版本。(代码我将在以后的日子里给出)。
OVER
法律声明:本文章受到知识产权法保护,任何单位或个人若需要转载此文,必需保证文章的完整性(未经作者许可的任何删节或改动将视为侵权行为)。若您需要转载,请务必注明文章出处为CSDN以保障网站的权益;请务必注明文章作者为刘铁猛,并向bladey@tom.com发送邮件,标明文章位置及用途。转载时请将此法律声明一并转载,谢谢!