回 帖 发 新 帖 刷新版面

主题:[原创]好东西大家一起分享:C#---Delegate(摘至baidu)

    C#是一个颇具争议的新兴语言,由Microsoft开发创造,以作为其VisualStudio.NET的基石,目前正处于第一个Beta版的发布阶段。C#结合了源自C++和Java的许多特性。Java社群对C#主要的批评在于,其声称C#只是一个蹩脚的Java克隆版本——与其说它是语言创新的成果,倒不如说是一桩诉讼的结果。而在C++社群里,主要的批评(也同时针对Java)是,C#只不过是另一个泛吹滥捧的私有语言(yetanotherover-hypedproprietarylanguage)。
     为了激发讨论,我将围绕一个testHarnessclass的设计来进行阐述。这个testHarnessclass能够让任何类别对static或non-static的classmethods进行注册,以便后续予以执行。Delegate型别正是实现testHarnessclass的核心。
     C#的DelegateTypeDelegate是一种函数指针,但与普通的函数指针相比,区别主要有三:
1)一个delegateobject一次可以搭载多个方法(methods)[译注1],而不是一次一个。当我们唤起一个搭载了多个方法(methods)的delegate,所有方法以其“被搭载到delegateobject的顺序”被依次唤起——稍候我们就来看看如何这样做。
2)一个delegateobject所搭载的方法(methods)并不需要属于同一个类别。一个delegateobject所搭载的所有方法(methods)必须具有相同的原型和形式。然而,这些方法(methods)可以即有static也有non-static,可以由一个或多个不同类别的成员组成。
3)一个delegatetype的声明在本质上是创建了一个新的subtypeinstance,该subtype派生自.NETlibraryframework的abstractbaseclassesDelegate或MulticastDelegate,它们提供一组publicmethods用以询访delegateobject或其搭载的方法(methods)
声明DelegateType:
一个delegatetype的声明一般由四部分组成:(a)访问级别;(b)关键字delegate;(c)返回型别,以及该delegatetype所搭载之方法的声明形式(signature);(d)delegatetype的名称,被放置于返回型别和方法的声明形式(signature)之间。例如,下面声明了一个publicdelegatetypeAction,用来搭载“没有参数并具有void返回型别”的方法:
publicdelegatevoidAction();一眼看去,这与函数定义惊人的相似;唯一的区别就是多了delegate关键字。增加该关键字的目的就在于:要通过关键字(keyword)——而非字元(token)——使普通的成员函数与其它形似的语法形式区别开来。这样就有了virtual,static,以及delegate用来区分各种函数和形似函数的语法形式。
如果一个delegatetype一次只搭载单独一个方法(method),那它就可以搭载任意返回型别及形式的成员函数。然而,如果一个delegatetype要同时搭载多个方法(methods),那么返回型别就必须是void[译注2]。例如,Action就可以用来搭载一个或者多个方法(method)。在testHarnessclass实现中,我们就将使用上述的Action声明。
定义DelegateHandle:
在C#中我们无法声明全局对象;每个对象定义必须是下述三种之一:局部对象;或者型别的对象成员;或者函数参数列表中的参数。现在我只向你展示delegatetype的声明。之后我们再来看如何将其声明为类别中的成员。
C#中的delegatetype与class,interface,以及arraytypes一样,属于referencetype。每个referencetype被分为两部分:
一个具名的句柄(namedhandle),由我们直接操纵;以及
一个该句柄所属型别的不具名对象(unamedobject),由我们通过句柄间接进行操纵。必须经由new显式的创建该对象。
定义referencetype是一个“两步走”的过程。当我们写:
ActiontheAction;的时候,theAction代表“delegatetypeAction之对象”的一个handle(句柄),其本身并非delegateobject。缺省情况下,它被设为null。如果我们试图在对其赋值(译注:assigned,即与相应型别的对象做attachment)之前就使用它,会发生编译期错误。例如,语句:
theAction();会唤起theAction所搭载的方法(method(s))。然而,除非它在定义之后、使用之前被无条件的赋值(译注:assigned,即与相应型别的对象做attachment),否则该语句会引发编译期错误并印出相关信息。
为DelegateObject分配空间:
在这一节中,为了以最小限度的涉及面继续进行阐述,我们需要访问一个静态方法(staticmethod)和一个非静态方法(non-staticmethod),就此我采用了一个Announceclass。该类别的announceDate静态方法(staticmethod)以longform的形式(使用完整单字的冗长形式)打印当前的日期到标准输出设备:
Monday,February26,2001非静态方法(non-staticmethod)announceTime以shortform的形式(较简短的表示形式)打印当前时间到标准输出设备:
00:58前两个数字代表小时,从午夜零时开始计算,后两个数字代表分钟。Announceclass使用了由.NETclassframework提供的DateTimeclass。Announce类别的定义如下所示。
publicclassAnnounce{publicstaticvoidannounceDate(){DateTimedt=DateTime.Now;Console.WriteLine("Today'sdateis{0}",dt.ToLongDateString());}publicvoidannounceTime(){DateTimedt=DateTime.Now;Console.WriteLine("Thecurrenttimenowis{0}",dt.ToShortTimeString());}}要让theAction搭载上述方法,我们必须使用new表达式创建一个Actiondelegatetype(译注:即创建一个该类别的对象)。要搭载静态方法,则传入构造函数的引数由三部分组成:该方法所属类别的名称;方法的名称;分隔两个名称用的dotoperator(.):
theAction=newAction(Announce.announceDate);要搭载非静态方法,则传入构造函数的引数也由三部分组成:该方法所属的类别对象名称;方法的名称;分隔两个名称用的dotoperator(.):
Announcean=newAnnounce();theAction=newAction(an.announceTime);可以注意到,theAction被直接赋值,事先没有做任何检查(比如,检查它是否已经指代一个堆中的对象,如果是,则先删除该对象)。在C#中,存在于managedheap(受托管的堆)中的对象由运行期环境对其施以垃圾收集动作(garbagecollected)。我们不需要显式的删除那些经由new表达式分配的对象。
在程序的managedheap(受托管的堆)中,new表达式既可以为独个对象做分配
HelloUsermyProg=newHelloUser();也可以为数组对象做分配
string[]messages=newstring[4];分配语句的形式为:型别的名称,后跟关键字new,后跟一对圆括弧(表示单个对象)或者方括号(表示数组对象)[1]。(在C#语言设计中的一个普遍特征就是,坚持使用单一明晰的形式来区别不同的功用。)
一个快速的概览:GarbageCollection(垃圾收集)
如下述数组对象所示,当我们在managedheap(受托管的堆)中为referencetype分配了空间:
int[]fib=newint[6]{1,1,2,3,5,8};对象自动的维护“指向它的句柄(handles)”之数目。在这个例子中,被fib所指向的数组对象有一个关联的引用计数器被初始化为1。如果我们现在初始化另一个句柄,使其指向fib所指代的数组对象:
int[]notfib=fib;这次初始化导致了对fib所指代数组对象的一次shallowcopy(浅层拷贝)。这就是说,notfib现在也指向fib所指向的数组对象。该数组对象所关联的引用计数变成了2。
如果我们经由notfib修改了数组中某个元素,比如
notfib[0]=0;这个改变对于fib也是可见的。如果这种对同一个对象的多重访问方式并非所需,我们就需要编写代码,做一个deepcopy(深层拷贝)。例如,
//分配另一个数组对象notfib=newint[6];//从notfib的第0个元素开始,//依次将fib中的元素拷贝到notfib中去。//见注释[2]fib.CopyTo(notfib,0);notfib现在并不指代fib所指代的那个对象了。先前被它们两个同时指向的那个对象将其关联的引用计数减去1。notfib所指代对象的初始引用计数为1。如果我们现在也将fib重新赋值为一个新的数组对象——例如,一个包含了Fibonacci数列前12个数值的数组:
fib=newint[12]{1,1,2,3,5,8,13,21,34,55,89,144};对于之前被fib所指代的那个数组对象,其现在的引用计数变成了0。在managedheap(受托管的堆)中,当垃圾收集器(garbagecollector)处于活动状态时,引用计数为0的对象被其作上删除标记。
定义ClassProperties:
现在让我们将delegateobject声明为testHarnessclass的一个私有静态(privatestatic)成员。例如[3],
publicclasstestHarness{publicdelegatevoidAction();staticprivateActiontheAction;//...}下一步我们要为这个delegate成员提供读写访问机制。在C#中,我们不要提供显式的内联方法(inlinemethods)用来读写非公有的数据成员。取而代之,我们为具名的属性(namedproperty)提供get和set访问符(accessors)。下面是个简单的delegateproperty。我们不妨将其称为Tester:
publicclasstestHarness{staticpublicActionTester{get{returntheAction;}set{Action=value;}}//...}Property(属性)既可以封装静态数据成员,也可以封装非静态数据成员。Tester就是delegatetypeAction的一个staticproperty(静态属性)。(可以注意到。我们将accessor定义为一个代码区块。编译器内部由此产生inlinemethod。)
get必须以property(属性)的型别作为返回型别。在这个例子中,其直接返回所封装的对象。如果采用“缓式分配(lazyallocation)”,get可以在初次被唤起的时候建构并存放好对象,以便后用。
类似的,如果我们希望property(属性)能够支持写入型访问,我们就提供setaccessor。set中的value是一个条件型关键字(conditional-keyword)。也就是说,value仅在setproperty中具有预定义的含义(译注:也就是说,value仅在set代码段中被看作一个关键字):其总是代表“该property(属性)之型别”的对象。在我们的例子中,value是Action型别的对象。在运行期间,其被绑定到赋值表达式的右侧。在下面的例子中,
Announcean=newAnnounce();testHarnes.Tester=newtestHarness.Action(an.announceTime);set以内联(inline)的方式被展开到Tester出现的地方。value对象被设置为由new表达式返回的对象。
唤起DelegateObject:
如之前所见,要唤起由delegate所搭载的方法,我们对delegate施加calloperator(圆括弧对):
testHarness.Tester();这一句唤起了Testerproperty的getaccessor;getaccessor返回theActiondelegatehandle。如果theAction在此刻并未指向一个delegateobject,那么就会有异常被抛出。从类别外部实行唤起动作的规范做法(delegate-test-and-execute,先实现代理,再测试,最后执行之)如下所示:
if(testHarness.Tester!=null)testHarness.Tester();对于testHarnessclass,我们的方法只简单的封装这样的测试:
staticpublicvoidrun(){if(theAction!=null)theAction();}关联多个DelegateObjects
要让一个delegate搭载多个方法,我们主要使用+=operator和-=operator。例如,设想我们定义了一个testHashtableclass。在构造函数中,我们把各个关联的测试加入到testHarness中:
publicclasstestHashtable{publicvoidtest0();publicvoidtest1();testHashtable(){testHarness.Tester+=newtestHarness.Action(test0);testHarness.Tester+=newtestHarness.Action(test1);}//...}同样,如果我们定义一个testArrayListclass,我们也在defaultconstructor中加入关联的测试。可以注意到,这些方法是静态的。
publicclasstestArrayList{staticpublicvoidtestCapacity();staticpublicvoidtestSearch();staticpublicvoidtestSort();testArrayList(){testHarness.Tester+=newtestHarness.Action(testCapacity);testHarness.Tester+=newtestHarness.Action(testSearch);testHarness.Tester+=newtestHarness.Action(testSort);}//...}当testHarness.run方法被唤起时,通常我们并不知道testHashtable和testArrayList中哪一个的方法先被唤起;这取决于它们构造函数被唤起的顺序。但我们可以知道的是,对于每个类别,其方法被唤起的顺序就是方法被加入delegate的顺序。
DelegateObjects与GarbageCollection(垃圾收集)
考察下列局部作用域中的代码段:
{Announcean=newAnnounce();testHarness.Tester+=newtestHarness.Action(an.announceTime);}当我们将一个非静态方法加入到delegateobject中之后,该方法的地址,以及“用来唤起该方法,指向类别对象的句柄(handle)”都被存储起来。这导致该类别对象所关联的引用计数自动增加。
an经由new表达式初始化之后,managedheap(受托管的堆)中的对象所关联的引用计数被初始化为1。当an被传给delegateobject的构造函数之后,Announce对象的引用计数增加到2。走出局部作用域之后,an的生存期结束,该引用计数减回到1——delegateobject还占用了一个。
    好消息是,如果有一个delegate引用了某对象的一个方法,那么可以保证该对象会直到“delegateobject不再引用该方法”的时候才会被施以垃圾收集处理[4]。我们不用担心对象会在自己眼皮底下被贸然清理掉了。坏消息是,该对象将持续存在(注:这可能是不必要的),直到delegateobject不再引用其方法为止。可以使用-=operator从delegateobject中移除该方法。例如下面修正版本的代码;在局部作用域中,announceTime先被设置、执行,然后又从delegateobject中被移除。
{Announcean=newAnnounce();Actionact=newtestHarness.Action(an.announceTime);testHarness.Tester+=act;testHarness.run();testHarness.Tester-=act;}我们对于设计testHashtableclass的初始想法是,实现一个析构函数用以移除在构造函数中加入的测试用方法。然而,C#中的析构函数调用机制与C++中的却不大相同[5]。C#的析构函数既不会因为对象生存期结束而跟着被唤起,也不会因为释放了对象最后一个引用句柄(referencehandle)而被直接唤起。事实上,析构函数仅在垃圾收集器作垃圾收集时才被调用,而施行垃圾收集的时机一般是无法预料的,甚至可以根本就没施行垃圾收集。
C#规定,资源去配动作被放进一个称为Dispose的方法中完成,用户可以直接调用该方法:
publicvoidDispose(){testHarness.Tester-=newtestHarness.Action(test0);testHarness.Tester-=newtestHarness.Action(test1);}如果某类别定义了一个析构函数,其通常都会唤起Dispose。
访问底层的类别接口:
让我们再回头看看先前的代码:
{Announcean=newAnnounce();Actionact=newtestHarness.Action(an.announceTime);testHarness.Tester+=act;testHarness.run();testHarness.Tester-=act;}另一种实现方案是,先检查Tester当前是否已经搭载了其它方法,如果是,则保存当前的委托列表(delegationlist),将Tester重置为act,然后调用run,最后将Tester恢复为原来的状态。
我们可以利用底层的Delegate类别接口来获知delegate实际搭载的方法数目。例如,
if(testHarness.Tester!=null&&testHarnest.GetInvocationList().Length!=0){ActionoldAct=testHarness.Tester;testHarness.Tester=act;testHarness.run();testHarness.Tester=oldAct;}else{...}GetInvocationList返回Delegateclassobjects数组,数组的每个元素即代表该delegate当前搭载的一个方法。Length是底层Arrayclass的一个property(属性)。Arrayclass实现了C#内建数组型别的语义[6]。
经由Delegateclass的Methodproperty,我们可以获取被搭载方法的全部运行期信息。如果方法是非静态的,那么经由Delegateclass的Targetproperty,我们更可以获取调用该方法之对象(译注:即该方法所属类别的那个对象)的全部运行期信息。在下面例子中,Delegate的methods(方法)和properties(属性)用红色表示:
If(testHarness.Tester!=null){Delegate[]methods=test.Tester.GetInvocationList();foreach(Delegatedinmethods){MethodInfotheFunction=d.Method;TypetheTarget=d.Target.GetType();//好的:现在我们可以知道delegate所搭载方法的全部信息}}总结
希望本文能够引起你对C#delegatetype的兴趣。我认为delegatetype为C#提供了一种创新性的“pointertoclassmethod(类别方法之指针)”机制。或许本文还引起了你对C#语言以及.NETclassframework的兴趣。
注释:
[1]对于C++程序员来说,有两点值得一题:(a)需要在对象的型别名称之后放一对圆括弧作为defaultconstructor,以及(b)用于数组下标的方括号要放在型别与数组名称之间。
[2]C#中内建的数组是一种由.NETclasslibrary提供的Arrayclass之对象。Arrayclass的静态方法和非静态方法都可以被C#内建数组对象使用。CopyTo是Array的一个非静态方法。
[3]与Java一样,C#中的成员声明包括其访问级别。缺省的访问级别是private。
[4]类似的,C++标准要求,被引用的临时对象必须直到引用的生存期结束时才能够被销毁。
[5]在内部实现中,析构函数甚至都不曾存在过。一个类别的析构函数会被转换成virtualFinalize方法。
[6]在C#中,一个条件判别式的结果必须得到Boolean型别。对Length值的直接判别,如if(testHarness.Length),并不是合法的条件判断。整型值无法被隐式的转换为Boolean值。
译注
[译注1]  在C#中,所谓“method(方法)”,其实就是指我们平常所理解的成员函数,其字面意义与“function(函数)”非常接近。
[译注2]  作者是就前述的那个delegatetypeAction声明而有此言。就一般而言,只要多个方法(methods)的返回型别相同并且参数也相同,就可以被同一个delegatetype搭载。

回复列表 (共4个回复)

沙发

建议楼主重新排版一下

板凳

谢谢指教

3 楼

高人

4 楼

不小心还以为自己看到了文言文

我来回复

您尚未登录,请登录后再回复。点此登录或注册