从面向对象编程的角度解析c#中的事件处理机制

时间:2022-08-30 23:06:00

摘要:c#中的事件处理机制是很多人学习c#过程中的难点。本文将从面向对象编程的角度解析微软的工程师们为什么会这样来设计c#事件处理机制。

1 现实生活中事件处理的三种模式

现实生活中,我们说事件处理,常常是这样一个情况:一个人发生了某种变动(比如孩子生病了),然后另外一个人(当然也可以是本人,为了和前面例子对应,这里假设是妈妈)做出相应的对策(送他上医院)。在这个过程中,发生了消息的传递。请注意区分以下概念:孩子生病了这是事件源,妈妈在第一时间内是不知道的,她需要等孩子发条信息(比如孩子表现出咳嗽,发烧)过来才知道,也就是说,这中间发生了消息的传递。是的,正因为如此,事件处理机制在以前也叫消息处理机制。

通过我们对现实生活中事件处理机制的分析,我们可以发现有三种模式:

a: 事件源主动向事件接收者报告消息,接收者收到消息做出响应。这就像孩子主动告诉妈妈:“我生病了”。这种方式的特征是:主动报告。

b: 事件接收者不断向事件源询问当前情况,是否有变动。就像妈妈问孩子,你是不是病了? 这种方式的特征是:事件源不主动报告消息,而是由事件接收者被动的不断的去询问。哎,孩子不懂事,没有办法,只能是妈妈多操心了。

c: “保姆模式”:你想想,孩子生病了,这是多大的事啊!妈妈紧张,爸爸不也紧张吗?爷爷奶奶更紧张!那难不成爸爸妈妈爷爷奶奶整天守候在孩子旁边,不停的问:“孩子,你生病了没有?生病了咱上医院去。”且不说爸爸妈妈爷爷奶奶还有别的事要忙,这样不停的问孩子,孩子没病也会闹出个病来啊。哎,对了,请个保姆。保姆守候在孩子旁边,只要孩子生病,就立即通知爸爸妈妈爷爷奶奶叔叔阿姨亲姑舅嫁出去的表姐还有八百里表叔等等各位关心宝宝的人。呵呵,这保姆对孩子来说是“保姆”,对于各位关心宝宝的人来说,就是他们的公共秘书,只要孩子一生病,就通知大伙。至于是孩子主动告诉保姆自己病了,还是保姆不停的问孩子是否病了呢,那是保姆的事,否则,如果孩子病了,保姆没有通知大伙,那就是保姆的失职。

2 事件处理机制的技术实现。

前面我们看了在现实生活中的事件处理机制,下面我们来看在程序设计中如何实现事件处理机制。

事件处理,说白了,就是一个对象请求另一个对象执行活动,在程序中,也就是函数调用。不要以为c#中的事件处理机制就不是函数调用了,还是,而且:只能是,只会是。我们程序中所有的逻辑,毫无例外都是通过函数的调用来进行组织的。

下面我们来仔细看看其具体的实现机制。

// 定义“生病参数”,此处仅考虑 “体温”
public   class   BabyILLEventArgs:EventArgs
{
float   Temprature;
public   BabyILLEventArgs( float   temprature)
{
this .Temprature = temprature;
}
}

// 定义委托,注意委托是一个类,当然可以象类一样定义
public   delegate   void   BabyIllEventHandler( object   sender,BabyILLEventAgrs args);
// 定义宝宝类
public   class   Baby
{
float   Temprature;

// 事件
public event BabyIllEventHander BabyIsILL;

// 孩子生病函数
public   void   BabyFallIll()
{
if   ( this .Temprature != 37 )
{   // 事件参数
BabyILLEventArgs e = new   BabyILLEventArgs(temprature);

// 激发事件
BabyIsIll( this ,e); }
}
}
// 定义大人类
public   class   Adult
{
public   Adult(Baby baby)
{
// 对“孩子生病”事件进行注册,只要孩子生病,就做出响应
baby.BabyIsIll += new   BabyIllEventHandler(GoToHospital);
}

// 送去医院函数
public   GoToHospital(   object   sender,BabyIllEventArgs e )
{
if   (e.Temptature > 40 || e.Temprature <</SPAN>35)
{
Console.Write(
"很严重,去省立医院就医");
return;
}
if(e.Temprature>39||e.Temprature<</SPAN>36)
{
Console.Write(
"比较严重,去市医院");
return;
}
else
{
Console.Write(
"病得较轻,去县医院");
return;
}
}
}

在上面的代码中,在Baby类中,定义了孩子生病事件,传递“体温”参数,在Adult类的构造函数中注册(也就是:登记)上事件,使得孩子的生病和大人的送去医院两个函数连接起来。连接的纽带就是那条:public eventBabyIllEventHander BabyIsILL;语句中声明的BabyIsILL。它就是孩子的保姆,通过它调用所有的亲戚朋友(都是Adult的实例)的GoToHospital()方法。

既然,事件处理机制仍然是函数调用,那为什么不直接调用呢?这就得从面向对象的角度来分析了,请看下节。

3 从面向对象编程的角度来理解c#中事件机制如此设计的理由

我文章的题目说,要从面向对象编程的角度分析微软为何如此设计c#的事件处理机制,现在,就来看一下其中缘由。

我们知道,面向对象编程的三大机制是:“封装”,“继承”,“多态”。第一条就是“封装”,封装使得你可以在某一段时间将主要精力集中在某段代码上。好了,现在,你把某部分代码写好了,但任何对象在这个世界上都不是孤立的对象,需要与外界接触。你需要调用别人的方法,你的方法要被别人调用。你能调用别人的方法是因为别人向你提供了“接口”;你的方法要被别人调用,你就要向别人提供接口。这里说的接口是一个广泛的概念,并不是我们程序定义中声明的“interface”。我们将我们的类,类的成员变量,成员函数声明为public,protected,internal。在给它们定义了一个安全限制级别的同时,更重要的是向相应的其它对象(public--所有;internal--程序集内部;protected--子类对象)“张贴”了一张“广告”:我的这些类,这些变量,这些方法,你们可以拿去用。换言之,你的所有可以被外界访问的东东,都是你提供的接口。

一方面是封装的必要性,另一方面,对外暴露接口也是必然。通过权衡,我们提出了:每个开发人员只要管自己的代码,并向别人提供你应该提供的接口就可以了;不要去管调用者如何调用你的代码,系统的设计者会定义好相互之间交互的接口协议,每个人都按照接口定义去实现代码,按照接口定义去调用代码。就像孩子生病了,孩子不要去管有多少人来照顾他,哪个医生来给他治病一样,孩子所需要做的就是告诉大家:我生病了。

事件正是这样一个对外的接口!!!我只要暴露出这样一个接口,我不管哪些对象来处理,它们怎么处理!!!正是因为如此,我们的Event前面常常声明为public,声明为private是没多大意义的。

既然事件是一个对外的接口,那就要有对外的一个“规约”,一个“协定”,这个规约协定就是“委托”。事件要告诉外部对象,是从谁哪里发出来的事件,有哪些具体的事件信息,那么这个“委托”就要定义事件源(Sender),事件信息(EventArgument)(说明:这里指的是委托用于事件的时候,委托还可以应用于其它场合,具体见文后补充)。而对于要处理这个事件的对象来说,自己的函数无疑也要遵守这个“委托”约定。从函数调用来说,事件处理函数无疑也要符合这个格式,才能实现正确的函数调用。也就是说对于事件来说,委托充当着事件这个对外接口的函数声明的“约定”。另外一面,对一个事件的响应可以是多个对象的相应函数处理(就像孩子生病,爸爸妈妈爷爷奶奶等都要去医院一样),这些对象的事件处理函数又都要遵守这个“约定”,于是,委托又成为了事件处理函数的“模板”,模板的概念在面向对象中就是“类”的概念,所以,我们常说委托是“函数的类”。

理解了委托,我们再会过头来看“Event”。说真的,“接口”还只能说是它的对外表现,其内在则是一个“函数的容器”。你想想,你暴露这么一个接口,别人遵守你的接口约定定义了函数,挂接上了你这个事件。你怎么在你事件发生的时候,调用这些人的这些函数?这就在声明事件的时候“Event”关键字的所在了。它里面肯定做了“手脚”来记住哪些人的哪些方法注册了我这个事件。是的,通过Reflector一看,你就知道了,一句简单的事件声明实际上变成了一个字段,两个方法,其中一个负责往字段上“挂接”(注册) 方法,另一个负责“移出”(注销)方法。比如,我们的那句public event BabyIllEventHander BabyIsILL声明,会在Baby类中变成下面这段程序:

// 1 一个初始化为NULL的“私有”委托类型字段
private   BabyIllEventHandler babyIll = null ;

// 2 一个允许对象登记事件的add_*方法
[MethodImplAttribute(MethodOptions.Synchronized)]
public   void   add_BabyIll(BabyIllEventHandler handler)
{
babyIll
= (BabyIllEventHandler)Delegate.Combine(babyIll,handler);
}

// 3 一个允许对象注销事件的remove_*方法
[MethodImplAttribute(MethodOptions.Synchronized)]
public   void   remove_BabyIll(BabyIllEventHandler handler)
{
babyIll
= (BabyIllEventHandler)Delegate.Remove(babyIll,handler);
}

  能够往里面添加和移出方法,它不就是一个函数的容器吗?是的,这些函数都是符合委托中定义的格式。正因为如此,很多其他地方的讲解也常说是“委托链表”,但我更愿意说是“容器”,显然,“容器”比“链表”更宽泛,且不说里面是不是真的是“链表”结构,即便现在是,将来会不会是?Java会不会是呢?“容器”才是其关键所在。

希望这篇文章,能帮助读者理解c#中的事件处理机制。谨以此文献给qinghu和qwliang,在我学习c#的过程中,他们对我的指导作用无可替代。