指针的本质我们都知道,就是其所指向成员的内存地址,函数指针就是要调用函数的内存地址了,地址就是赤裸裸的地址,除了这一地址信息之外其它的一无所有,如关于参数和返回值等的数据类型信息,而委托就不同,委托并不单纯的是调用函数的地址,它本质上是一个类,经过对函数指针的这么一类型化的封装,包含进了数据类型等很多信息,因此委托 被称之为类型安全的指针。青出于蓝胜于蓝,委托还有比函数指针强大的地方,回头看一下前面C++的一行代码:
//注意,此处必须声明为静态方法。
public:
static void mySay()
{}
为什么必须声明为静态方法?再看一下C#的代码:
//注意,此处可以不声明为静态方法 public static void mySay(){}
为什么这里可以不声明为静态方法?这里我们可以清楚的是,C++中用函数指针来调用的函数必须是静态的(C语言里这点无关紧要),而在C#中用委托来调用的函数可以是静态的,也可以不是.还不仅仅如此,函数指针一次只能实现对一个函数的调用,而委托没有这个限制,可以实现一次调用多个函数.
C#委托的代码写在下面:
class liyufeng { public static void mySay() { System.Console.WriteLine("I Like You,Do you know,if you don’t know,now I must tell you ,tell you!"); } } class goodfriend { public delegate void middleSay(); public static void friendSay(middleSay say) { say(); } } class beautifulpark { static void Main(string[] args) { goodfriend.friendSay(liyufeng.mySay); System.Console.Read(); } }
首先看委托的声明public delegate void middleSay(),前面提到过,委托是函数指针封装成了对象,也就是说委托也应该是一个类,那么这里是通过Delegate关键字,而不是用声明类的关键字Class,也很显然,从上面这句代码无论如何也看不出来委托是一个类,表面上看它似乎是一个方法。要弄清楚这一点,我们需要用到ILdasm这个查看MSIL代码的工具,通过它便可一目了然,其实.NET编译器背后为我们做了很多的工作。下面是声明委托middleSay的IL代码:
.class auto ansi sealed nested public middleSay extends [mscorlib]System.MulticastDelegate { .method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed { } .method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke(class [mscorlib]System.AsyncCallback callback, object 'object') runtime managed { } .method public hidebysig newslot virtual instance void EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed { } .method public hidebysig newslot virtual instance void Invoke() runtime managed { } }
关于上面IL代码的语法我们不必太理会,也可以明白的看出我们声明的委托middleSay的确是一个从MulticastDelegate继承而来的类,并且有一个构造函数和三个成员函数。只是.NET编译器不允许我们自己直接从MulticastDelegate显式继承,否则上面的代码我们完全可以自己实现。看一下MulticastDelegate类的声明:
public abstract class MulticastDelegate : Delegate { }
可以看出它是一个抽象类,并且从Delegate继承;前面文章曾提到过,我们可以利用委托一次调用多个函数,所以这里的MulticastDelegate,从字面也可大概猜出它的功能,它主要是为我们准备了一个委托对象的链表,从而为我们调用多个委托函数提供了支持,也就是大家常说的多播委托,而它的父类Delegate,为调用一个委托函数提供必要的支持,微软本意是这么设计的,但是其实我们声明的委托全部是从MulticastDelegate继承而来的,也就说单播委托和多播委托的实现机制是相同的,无论单播还是多播都会被MulticastDelegate保存到自身的链表中,在进行委托函数调用时,都是一个个的按次序轮流调用,直到把委托链表中的委托对象全部都调用一遍。关于MulticastDelegate和Delegate,应该说是微软设计上的一个失误,Jeffrey Richter先生在书中提到微软应该在.NET未来的版本中将这两个类合并到一起,但到目前的2.0版本,它们仍然还未合并。其实从面向对象的角度来理解,感觉倒也不错,Delegate为父类,提供了委托基本的属性和行为,像单播委托;而MulticastDelegate从Delegate继承,理所当然保留父类的一切,并且为了支持多播委托,而进行了功能上的扩充。如果从其本身功能的角度理解,可能会有点迷惑,既然Delegate主要提供单播委托支持,而MulticastDelegate主要提供多播委托支持,那么单播委托理论上应该从Delegate继承比较适宜,而多播委托应该从MulticastDelegate继承较为妥当,然而不尽然的是两者均从MulticastDelegate继承而来,这么一来确实有点让人摸不着头脑了。(有关这里的详细内容请参看Jeffrey Richter先生原著李建忠先生翻译的《.NET框架程序设计(修订版)》) 下面来看对委托函数的调用
public static void friendSay(middleSay say) { say(); }
看样子和我们调用一个函数的语法没什么两样,只是这里是通过委托变量来调用的(此处,say是一个middleSay委托类型的变量),如果声明的委托原型有参数的话,比如 public delegate void middleSay(string myStr);在这里调用的时候我们还可以给委托传递参数,像这样:say("I Love you"); 但实际上我们知道,say只是一个委托类型的变量,它的内容是托管堆上该委托变量的引用地址,而不像函数指针,就是一个真实的函数地址,我们通过函数指针调用函数,直接跳到该地址处就可以了,但通过委托变量进行调用这点显然是行不通的。这个时候.NET编译器为我们带来了福音,还记得前面的IL代码吗?编译器为我们生成了三个实例方法,其中有一个Invoke()方法,而且该方法的返回值和参数与我们声明的委托middleSay是完全匹配的,关键就是它了,编译器在这里遇到say(); 会非常自然的转换成对这个委托对象的调用,也就是像这样:say.Invoke(); 这点我们通过IL代码也可以看出来,下面是该委托调用的IL代码:
.method public hidebysig static void friendSay(class ConsoleApplication1.goodfriend/middleSay say) cil managed { .maxstack 8 nop ldarg.0 callvirt instance void ConsoleApplication1.goodfriend/middleSay::Invoke() nop ret }
看起来应该是可以了,但还有一个问题,Invoke()方法如何知道要调用委托方法的信息,比如我们这里要调用的是liyufeng类中的mySay方法,编译器是如何让Invoke定位到liyufeng类的方法mySay呢?这就又需要MulticastDelegate和Delegate的配合了,想看到其中秘密还得借助ILdasm这个工具,用它来查看mscorlib.dll这个程序集,再看Delegate类的信息,可以看到有_methodPtr和_target两个私有字段,_methodPtr就是用来标识委托方法的,我的理解它就和我们真正的函数指针差不多了,是一个函数地址;_target是用来标识委托方法所在的对象,当然我这个例中由于需要回调的方法mySay是一个静态方法,所以_target会被编译器设置为null;如果把mySay改为一个实例方法的话,那么_target就会指向一个对象实例,此处即应该为liyufeng类的一个实例。编译器在通过Invoke对委托方法进行调用时就是根据_methodPtr和_target进行定位的。讨论到这里,不知道大家还记不记得上一篇文章中有个问题,就是在C++中通过函数指针进行函数调用,该函数如果被封装在类中的话,必须是静态函数,这是因为在C++中,调用非静态函数时,总会隐含的传递一个当前对象实例的this指针,以便标识是对当前对象成员的调用,而调用静态函数时就不会有这个隐含的this指针,所以如果你通过函数指针对非静态函数进行调用时,由于被调用的非静态函数多了一个this参数,无法和声明的函数指针原型匹配,这就是为什么必须为静态函数的原因了。.NET下编译器为我们生成的_target是不是和this指针相似呢?至少通过_target私有字段,.NET为我们解决了调用实例方法(也就是非静态函数)的难题。 总之,.NET通过MulticastDelegate和Delegate两个类,再加上编译器的从中配合,三者很好的演绎了委托这一强大的技术,虽然我们再平时的编成中对他们接触的可能不多,但我感觉认识一下他们也是不错的,至少能加深对委托的理解。
(编辑:aniston)
|