首先我们以书中所介绍的委托的由来为起始点展开话题:
在C或C++中有时为了开启一个子线程,我们会用到如下的方法:
HANDLE hThread;
DWORD ThreadID;
hThread = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadFunc,NULL,0,&ThreadID);
以上语句中的ThreadFunc()为C++中的一个全局方法。因为C++或者C语言都是允许面向过程的,因此全局函数是可以存在的。但大家不妨试想一下,如果是C#或者Java这种纯面向对象的语言,我们要怎么做呢?
思考一下,在C#中你可以用如下的代码来实现相同的效果吗?
Tread subTread = new Tread();
subTread.Start(EntryPoint);
这样做是明显不对的!在C++中函数名EntryPoint实际上就是一个全局函数指针,它表明了子线程的入口地址。然而,在C#中这样的情况是绝对不存在的!原因很简单——作为一门面向对象的语言,C#中很少有方法是独立于对象而存在的。
在C#中调用方法时,这些方法通常都要与一个类的实例相关联。也就是说,如果要传递一个方法,那么就必须把该方法的相关细节包装在一种新型的对象中。于是,伴随着这种需要,委托随之诞生。
以上只是《C#高级编程》一书中所提到的委托的产生过程,在这里我并不想深究其中的机制,而是以此为线索,深层次的挖掘C#中委托的实质用途及其优势所在。
这样一来,首先单纯根据委托的定义可知,委托可以简单的包装方法的细节:
1.简单的包装方法
首先,我们定义委托类型void MyDelegete();
这里有必要强调一下委托的声明规则:委托使用关键字delegate来声明,因其本身就是一种类型,故可以在类内声明,亦可以独立于类而声明。关键字之后是此委托类型的实体部分。不难看出其形式与方法的声明相近,但不包含函数体。
其实,我们大可理解为委托本身就是定义了一类方法,一类其返回值与参数表和委托实体定义形式完全相同的方法。只有符合委托规则的方法,才可以挂接于此委托的实例对象之上。这也是为什么,我将整个void MyDelegete()称作委托类型的原因。
接下来我们要在某一个类内部,运用先前定义的委托类型实例化一个委托对象MyObjdelegate,并为其挂载一个与之委托类型(无返回值,无参数)相符的自定义方法MyFun(),这也就是所谓“将方法MyFun()的相关细节包装于委托实例MyObjdelegate中”。而后即可在相应的按钮单击事件中利用委托对象MyObjdelegate调用其已经包装的方法MyFun()了。
以上即为由委托定义而得来的委托的简单用法。不过,以上这个实例显然没有什么实际用途,因为不涉及类间的相互调用,我们大可在事件中直接调用本类的方法MyFun(),而不必再多绕个弯依靠委托来实现。
不过,如果涉及到跨类的方法调用时,使用委托会怎么样呢?
2.使用本类的委托对象包装其他类的方法
这段代码和上一段代码的意思大同小异,委托类型void MyDelegete()的声明被我移到了类的外面,当然这不影响大局。主要区别只在于这段代码涉及到了使用本类委托对象跨类包装其他类的方法细节问题——我把原来主类中的MyFun()方法移到了新声明的一个类ObjCls中,然后再靠主类中的委托对象去调用这个方法。
或许你可能会说,即使我不用委托,也可以通过在主类中声明其他的类对象,然后通过这个类对象来调用其他类的public方法。这样一来,使用委托依然没有什么实实在在的意义。
那么,呵…… 我们不妨继续耐心而深入的挖掘委托更深层次的用途
3.在本类中操控其他类的委托对象
这段代码相对于上段代码产生了部分改动,不过,意思上大体相同:这次的委托对象定义在次类中,不过仅仅是一个引用,我并没有在次类中对其进行任何的初始化。相反,次类中委托对象方法挂载的工作是在主类中进行的:首先,我在主类中定义次类的实体对象,而后通过这个对象将主类的DelegateFun()方法挂接到次类的委托对象上。这样当次类的方法MyFun()被调用(事实上,你也可以将这个方法看做一个事件,当事件被触发时),就会调用主类中事先写好的DelegateFun()方法。
写到这里,我不知道你有没有看出使用委托的优势所在。其实,将第二段、第三段代码反复比对之后你就会发现二者有明显的不同!我们暂且撇开委托这种机制不谈,但看两段代码中本类与次类方法交互的手法便不难发现:二、三同为一个类调用另一个类中的方法,前者为一个类控制自身调用(另一个类的)一些方法;而后者却是一个类在控制另一个类调用哪些方法。
呵呵……悟性好的人应该已经联想到了,这种手法很像C#中的事件机制。而事实上,C#中的事件就是委托的一种特殊形式。
4.C#中事件的实现
首先,我先构建一个用户控件工程,代码如下:
这里,稍稍说明一下控件中事件的声明。事件的声明需使用关键字event,大致的形式是event +(某委托类型)+ 一个实例对象。
而后我建立一个Demo程序,来调用这个用户控件,代码如下:
在工程中引入用户控件,打开其事件窗口可以找到我们事先定义的事件MyEvent,双击就可以在其中写入代码。这里,我用一个MessageBox来进行测试。
编译通过后,单击控件上的button就会弹出我们的MessageBox,事件调用成功!
呵呵……发现了吗?这段代码所隐含事件机制的雏形实际上就是第三段代码中所实现的效果。只不过这里我们是用一个具体的窗口类控制了其所引入控件类中的委托对象。
看到类构造方法中的InitializeComponent()方法了吗?双击转到定义,在Windows窗体设计器生成的代码中,在你所引入的控件属性初始化模块下有这样一句代码:this.userControl11.MyEvent += new Ctrleven.UserControl1.MyEventHander(this.userControl11_MyEvent); 你可以拿来跟第三段代码中的那句 objcls.MyObjdelegate += new ObjCls.MyDelegete(DelegateFun); 对比一下,发现了吗?是一样的!
所以,C#中的事件本身就是一种(多重)委托机制,凭借关键字event,使得我们在控件中定义好的事件专用委托实例对象在顶层的Demo程序的事件窗口中被自动识别。也就是说,即使不用事件,纯用委托,事件的机制一样可以实现,问题只是这样的委托对象无法轻易被外界识别罢了。
5.纯用委托仿真控件的事件机制
首先,我们依然建立一个控件项目,比之上一段代码作如下调整:
在这里我们去掉了event关键字,改用纯委托对象来实现。
而后,同样建立一个上层的Demo程序来调用这个控件,同样比之上一段代码要作些许细微的调整:
看到区别了吗?呵呵…… 此时InitializeComponent()关于事件挂接的那句话已经不见了,因为这次的MyEvent并不是一个事件专用的委托对象,而是一个普通的委托实例对象,不会自动被C#的编辑器识别。所以,我在主窗口类的构造方法中人为添加了一句代码:
this.userControl11.MyEvent += new Ctrleven.UserControl1.MyEventHander(this.VirtualEvent);
它的作用其实和上一段代码中那句是相同的,就本质而言都是在一个类中操控另一个类的委托对象而已,只不过因为我们没有严格按照C#中的事件声明方式来实现事件机制,因此,这句代码C#的编辑器不会很友好的替你生成,而是要靠你自己来写。呵呵……
可见,程序运行的效果和上一段代码大致相同。
在这里,我想纠正大家的几个误区。平时,我经常听到人们谈论,说所谓委托就是将自己写好的一段代码作为参数传入另一个方法中。没错,用委托的确可以实现类似的说法,比方说某一个方法的参数表中的一个参数是委托类型,这样子的确是可以的。但是,可以实现这种效果的先决要素首先是我们可以利用委托来传递一段代码(也就是一个方法)。所以说真正起到传递方法效果的不是方法中的参数,而是委托对象本身。使用委托对象本身来实现跨类、跨线程甚至跨空间传递方法,这种实现形式的使用频率要比仅仅将委托作为参数,以实现将代码传入另一个方法这样的实现形式来的高得多得多。
好,写到这里,我想应该可以稍微阐述一下我自己的观点了。呵呵……
个人认为,委托最大的优势所在就是——其兼有对象和方法二者的性质。我们既可以将一个委托实例对象作为对象来看待甚至操控,亦可以将其作为一个现成的方法来调用。(未完待续)