C++转向C# 的疑惑:事件的机制究竟是什么?

来源:互联网 发布:多目标优化问题 编辑:程序博客网 时间:2024/06/06 03:53

转自:http://blog.csdn.net/zhuweisky/article/details/415669

C++转向C# 的疑惑:事件的机制究竟是什么?

 

   C++是如此的强大,以至于你可以用它做任何想做的事情,下至各种系统级开发,上至各种大型应用程序。但是我们经常听人说,“C++不是完全的面向对象语言”,且不论这一说法是否正确 ,然而有一个事实是很明显的,那就是 C++并没有直接提供对属性、事件等面向对象、面向组件编程常用特性的支持,虽然我们可以通过各种技术自己实现这些特性,但这无疑会大大降低开发速度。C#中提供了属性和事件,这是令人兴奋的!拥有了这两样特性,我们可以方便的做很多事情,而且如果应用得当,可以大大增强程序的可维护性、健壮性和可扩展性等。可以这么说,C#程序主要是依靠事件机制驱动的,事件的重要性是不言而喻的。

    C#很方便,且继承了C++的强大,但是对于我们,C#隐藏了太多的东西。我觉得不挖掘出这些东西,我们就很难娴熟地驾驭C#

    刚从C++转向C#的程序员,对事件的机制模模糊糊,感觉有些东西很不清晰,那么本文就为你揭开这层面纱。先从C++的函数指针说起。

 

一.C++函数指针

每一个熟练的C++程序员一定对函数指针的使用都有自己的一番心得体会,并且乐在其中。可以说,函数指针是C++中极富技巧性的东东,C++初学者看到它往往就躲开了,而C++高手却把它当作利器中的利器。

函数指针,也是一种指针,既然在C++中指针代表某一块内存的首地址,那么函数指针当然也不例外,只是函数指针指向的内存中存放的是可执行代码。先来看看一个函数指针的例子。

//C++

int (*funp)(string ) ; //函数指针声明

...  ...

int GetNumber(string name)

{

    ...  ...

}

上面声明了一个函数指针funp ,这个函数指针指向的函数要具有这样的签名,该函数接受一个string类型的参数,并且返回一个int 。而GetNumber正好是一个这样的函数,所以可以这么做:

//C++

funp = &GetNumber ;

(*funp)(“sky”) ; //直接用 funp(“sky”) ;也可以。

更进一步,我们可以用typedef定义一个函数指针类型。

//C++

typedef int *FunP)(string  //定义函数指针类型FunP

FunP funp //定义一个FunP型的函数指针

funp = &GetNumber ;

(*funp)(“sky”) ; //用函数指针进行函数调用

 

二.C#委托delegate

C#中的delegate就相当于一个函数指针类型的抽象,一个delegate特化就相当于C++中的一个函数指针类型。如

//C#

delegate int FunPstring name //定义一个委托特化

委托特特化与C++中的函数指针相比较,优势在哪里了?C++中的函数指针是赤裸裸的,没有任何编译期的安全检查,如果函数指针所指向的内存中存放的并不是函数指针声明的类型,那么这个错误只能在运行期检测到。而C#中的委托把这种错误的检测从运行期提前到了编译期,也就是说C#中的委托是类型安全的函数指针。那么这是怎么做到的?且先看看下面的示例代码:

//C#

class Example

{

    private FunP funp ; //定义一个委托特化实例

    public Example()

    {

        this.funp += new FunP(this.GetNumber) ;

}

    private int GetNumber(string name){ ......}

    ......

}

语句this.funp += new FunP(this.GetNumber) ;在编译时就会检查GetNumber的签名与FunP的签名是否一致,如果不一致,就会发出编译错误。再来看看该语句中的“+=”意味着什么,该语句可以写成这样

this.funp = this.funp + new FunP(this.GetNumber) ;

    你也许已经看出来了,是的,我们可以将funp想象为一个FunP型的容器,比如队列吧,然后我们可以向里面添加同样签名的函数指针,而当执行this.funp();时,就会按照顺序一一调用队列中的函数指针指向的函数。

 

三.C#事件

C#程序大多都是事件驱动的,在讲事件的机制之前我想先讲讲事件驱动的基本模型,了解这个模型对理解事件机制是很有帮助的。

在这个模型中主要涉及两个类,我称之为事件发布类Provider和事件预定类Master,这有点像服务与客户的关系。图1是这个模型的简单示意图。首先是Provider发布事件,接着Master预定事件,然后Provider触发事件,最后调用Master的事件处理函数处理事件。

再来看看Master类与Provider类的耦合关系。Provider类不需要了解Master类的任何信息,甚至,Provider类不需要知道Master类的存在,所以Provider类是完全独立的、可复用的。一般地,.NET Framework中提供的各个控件都可以看作是Provider类,它们只负责发布事件和触发事件,至于是否有用户预定该事件和处理该事件,它们并不关心。而Master类必须要知道Provider类的某些信息,如Provider类发布了哪些事件,这些事件对应的事件信息类是如何定义的,因为Master类是从事件信息类实例中得到对应的事件的相关信息的。所以Master类高度依赖Provider类,如果想在切断与Provider类联系的情况下,单单复用Master类几乎是不可能的。

                  图1  事件驱动的基本模型

 

事件是一种特殊的委托特化实例,而这种委托特化的签名必须遵从特定的格式,如

public delegate void EventHandler(object source ,MyEventArgs e) ;

其中,source表示事件的发生源,一般是某个控件或窗体;MyEventArgs这个类必须从EventArgs类继承,该类用于封装与事件相关的信息。

Provider类可按如下代码声明事件:

public event EventHandler OkClick ;//发布事件

这也就是发布事件。既然事件是一个委托特化实例,那么它也相当于一个函数指针队列,初始时这个队列是空的。

另外我们要在Master类中定义事件处理函数,以便在事件发生时调用该处理函数。

private void Process_OkClick (object source ,MyEventArgs e)

{......}

注意,该函数的签名必须与上面的EventHandler的签名完全一致。

接下来,我们可以向OkClick这个队列中添加函数指针。像这样:

OkClick  += new EventHandler(Process_OkClick) ;//预定事件,注册

这就是在Master中预定事件。

发布事件的类Provider可以在适当的时候触发事件,像这样

this.OkClick();

要正确理解“触发事件”的意思。

“触发事件”并不是“使事件发生”,而是“当事件出现(发生)时,使事件处理函数执行(即调用事件处理函数)”。

上面说“发布事件的类Provider可以在适当的时候触发事件”,这个“适当的时候”就是我们指定的事件发生的时候。

比如,系统本身就可以有很多事件发生,如鼠标左键按下,这表示事件发生,此时系统就触发相应的事件,使对应的事件处理函数执行。触发事件的本质是让程序依次跳转到函数指针队列中的指针指向的地址,去执行相应的函数。

 

四.事件与继承

通常会有这样的情况出现:基类中定义了一个事件处理函数,并且将此事件处理函数注册到了某个预定的事件中,然后有一个子类从此基类派生,当然了,对于那个预定的事件,子类往往有自己的特定处理方式,而不需要使用或不能使用基类提供的处理方法,那么该怎么做了?像下面这样可以吗?

public class MyEventArgs: System.EventArgs

    {

          //此处定义自己的事件信息的结构

    }

 

//定义一个委托特化

    public delegate void EventHandler(object source ,MyEventArgs e) ;

   

class Provider   //事件发布类

    {

       public event EventHandler OkClick ;//发布事件

    }

 

    class Master_base  //事件预定类

    {

       protected Provider pro ;

       public Master_base()

       {

           pro = new Provider() ;

           //预定事件

           pro.OkClick += new EventHandler(this.Process_OkClick) ; //注册      }

       

        //事件处理函数

        private void Process_OkClick (object source ,MyEventArgs e)

       {

           //处理事件的代码

       }

    }

 

    class Master_derived :Master_base //从Master_base继承

    {

       public Master_derived()

       {

           //注册自己的事件处理函数

           pro.OkClick += new EventHandler(this.Process_OkClick_Myself) ;         }

 

       //子类自己的事件处理函数

private void Process_OkClick_Myself (object source ,MyEventArgs e)

       {

           //处理事件的代码

       }

    }

 

    class MainClass

    {

       [STAThread]

       static void Main(string[] args)

       {

           Master_derived example1 = new Master_derived() ;

           //使example1通过继承得到的成员pro的OkClick被触发

//伪码:

example1.pro. OkClick() ;

       }

    }

即当Main函数执行到伪码部分时,Master_derived类的事件处理函数Process_OkClick_Myself会被调用吗?而其基类Master_base的事件处理函数Process_OkClick也会被调用吗?

答案是都会被调用。你可能很意外。为什么基类的事件处理函数Process_OkClick也会被调用了?而且Process_OkClick还是private的啊!因为它是private,所以继承类Master_derived根本就不会知道它的存在,可它还是被调用了,这到底发生了什么?

有一点是我们都知道的,那就是当创建子类对象时,基类的构造函数会被首先调用,即以下语句执行时:

           Master_derived example1 = new Master_derived() ;

Master_base的构造函数会首先执行,也就是下面的代码会执行:

           pro = new Provider() ;

           pro.OkClick += new EventHandler(this.Process_OkClick) ;

所以,基类的事件处理函数Process_OkClick的地址(即函数指针)已经被保存在pro的OkClick这个指针队列中了,尽管Process_OkClick是private的。

 

我们可以使用指针刺破访问限定符带给我们的束缚,因为我们并没有通过对象这个间接层去访问它的成员,而是直接通过对象的成员的地址去访问这个成员。

 

当基类的构造函数执行完后,子类Master_derived的构造函数继续执行,这时子类的事件处理函数Process_OkClick_Myself的指针也被添加到proOkClick指针队列中了,所以,当预定的事件触发时,指针队列中的各个指针指向的函数就会依次执行。这就是为什么子类和基类的事件处理函数都会被调用的原因了。

现在的问题是,子类使用者并不希望基类的事件处理函数Process_OkClick被调用,因为子类已经有了自己更好的替代方案Process_OkClick_Myself,那么在子类对象中如何抑制基类的事件处理函数被调用了?其实很简单,你看到了事件注册使用的是“+=”,是吧,自然如果需要“注销”的话,用“-=”就可以了啊。我们将子类的构造函数改成下面这样就ok了:

public Master_derived()

       {

//注销基类的事件处理函数

           pro.OkClick -= new EventHandler(base.Process_OkClick) ;

           //注册自己的事件处理函数

           pro.OkClick += new EventHandler(this.Process_OkClick_Myself) ;         }

另外要注意的是,为了使子类能够注销基类的事件处理函数Process_OkClick,子类必须能够“看见” 这个函数这就需要将基类的Process_OkClick由private改为protected 。

    现在再执行一下Main函数,你会发现只有子类的事件处理函数被调用了,而这正是我们想要达到的目的。

 

    讲到这里,你可能已经对事件的机制有了一个比较清楚的了解了。在C#中事件是如此重要,只要你程序稍微复杂一点,几乎就会涉及到它。事件是如此的灵活,它为类间通信提供了一种优雅而又高效的解决方案,而且,它使程序的可维护性、可扩展性、可复用性都大大增强。

原创粉丝点击