又是提起委托,我在想的是众人必定皆恼怒,有关这个东西的谈论网上已悠悠不尽,何况我也不是赶时髦,追潮流的人,因此必然无法标新立异;只是把我学习的一些感受作以记录,便于日后回忆,也愿对各位朋友有点帮助。
对于学习C/C++语言的朋友,函数指针相信都不会觉得遥远和陌生吧,不管你对它是恨是爱,还是它令你忧伤令你逍遥自在;如果现今你还在使用C/C++,驰骋在Win32平台下怡然自乐,那么有它相伴身边你肯定不会觉得寂寞;如果你已告别C/C++,徜徉在.NET平台下悠然自得,那么没有了它相伴你也肯定不会觉得失落,因为一个名曰委托(Delegete)的朋友已翩然来到我们身边,所以我们用不着遗憾,更用不着千年一叹,因为委托可以称得上是函数指针的改头换面。
地球人都知道,函数指针的主要作用就是用它来调用函数的,那一个函数好端端的为什么不用人家的名字直接调用,偏偏的拐个弯用指针来调用呢?当然普通的调用直接用名字就可以了,但当我们想把函数作为参数来传递,就得靠函数指针;还有在程序界大名鼎鼎的回调函数(CallBack Function),它风光的背后也是函数指针在默默奉献。那么在.NET托管环境下,微软狠心割舍了函数指针这一不求名利的功臣,但没让我们失望的是增添了委托这一良将,把函数指针的功能发挥的淋漓尽致。还是来回忆一下我们的老朋友函数指针,正好这几天我也正在实行一惊天计划,准备对我暗恋已久的一女孩说出心里话,无奈我没经验又害羞,多次欲言却又口难开,所以准备托一好友帮忙先给她点暗示,完了我再随后出击,借写本文机会来场纸上谈兵。首先用函数指针模拟一下这个计划,妥当与否,姑且不作理论。
class liyufeng
{
//注意,此处必须声明为静态方法。
public:
static void mySay()
{
//我准备对女孩说的话,苦思冥想不知作何言
语,各位有经验的朋友请指点一二,万分感激
printf("I Like You,Do you know,if you
don’t know,now I must tell you ,tell you!");
}
};
上面这个类用我大名的拼音来命名,算是代表我这个无缘爱情的苦命人。我要说的话由静态方法mySay来说出,此处只是输出到控制台,要是真能对人家说了就高兴死俺了。
classgoodfriend
{
//这里的声明就是我们熟悉的函数指针了,我说话的那个mySay方法就由这个函数指针代劳,传递给朋友的friendSay方法.
typedef void (*middleSay)();
//这里利用函数指针把函数作为参数传递,朋友的friendSay方法说话,就是函数指针middleSay说话,到最后其实就是我自己的mySay方法说话
public:
static void friendSay(middleSay say)
{
say();
}
};
上面的这个类代表我的好朋友,姓名就不透露了,免得太多的像我这样的不敢对女孩表白的人都去找他这个爱管闲事的家伙,哦,他可就忙不过来啦。我的话呢通过这里的friendSay方法逐句逐字的说给女孩听。朋友代表我和女孩见面的地点定在一个美丽的公园里。
classbeautifulpark
{
//注意:如果在Visual2005里建立一个Win32的控制台程序的话,主函数是不应该封装在类里的,在此只是为了说明和同下边的委托版本保持一致的需要。嘿嘿!总得找个约会地点嘛;运行本程序的话,下边主函数中的内容直接拷贝即可
int _tmain(int argc, _TCHAR* argv[])
{
int c;
//把俺说话的mySay方法传递给朋友的friendSay方法,关键是这个函数指针哦,朋友的friendSay方法才能接受俺的mySay方法(其实关键是一顿美味可口的饭菜)
goodfriend::friendSay(liyufeng::mySay);
scanf_s("%d",&c);
return 0;
}
}
我和朋友一起先到公园,小心翼翼的把我说话的那个方法mySay给了朋友的friendSay方法,为什么要小心?函数指针嘛,不是个省心的东西,当然得小心喽。好了,后边的事情我就不得而知了,天知道女孩听了俺的话作何反应。
我的话说完了,同时和大家一起回忆了函数指针,没经历过亘古悠香的C/C++语言而是直接和.NET下语言做亲密接触的朋友,看了上面的几行代码有没有感觉似曾相识?没有的话,下面我再来模拟一下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!");
}
}
这个liyufeng类和上面的C++版本的不同的只是输出到控制台用的方法不一样。
class goodfriend
{
//这里使用delegate关键字声明.NET下的委托类型,同样,我说话的那个mySay方法就由这个委托代劳,传递给朋友的friendSay方法
public delegate void middleSay();
//这里利用委托把函数作为参数传递,朋友的friendSay方法说话,就是委托middleSay说话,到最后其实就是我自己的mySay方法说话
public static void friendSay(middleSay say)
{
say();
}
}
我的好朋友goodfriend类和前边的C++版本非常相似,唯一不同的是指针换成了委托,但完成的是同样的功能。
class beautifulpark
{
static void Main(string[] args)
{
//把俺说话的mySay方法传递给朋友的friendSay方法,关键是这个委托哦,朋友的friendSay方法才能接受俺的mySay方法(其实关键还是一顿美味可口的饭菜)
goodfriend.friendSay(liyufeng.mySay);
System.Console.Read();
}
}
好了,我追求缘分的计划也用委托班门弄斧的模拟了一番,到此为止,熟悉函数指针还没接触过委托的朋友,还有熟悉委托却对函数指针陌生的朋友,回头看一下,函数指针和委托是否颇为相似?在我初次接触委托的时候,也是被这个家伙搞的糊里糊涂,后来明白了,在.NET托管环境下的相关语言都是以面向对象作为基础来构建设计,一切都是对象,那么在这个对象丛生的世界里,摆脱对象这件外衣的成员是不允许存在的,也就是所有的成员都是从对象的老祖先Object派生而来。指针作为C/C++中一种特殊的数据类型,如果想在托管环境下生存的话,必然也是Object的后代,所谓入乡随俗,自然的也得披上对象的行套;所以对于函数指针而言,便是以一个对象封装了我们想要调用函数的所有相关信息,可以肯定这其中包含我们要调用函数的地址信息。当然C#等语言也只是对函数指针进行了类型化,给它改了个头,换了个面,起了个名叫Delegate,中文唤作委托;而对于其它数据类型的指针,并没有这么做,因此也就意味着我们是不可以像在C/C++中那样,自由自在的随意操作内存,这都是出于安全性的考虑。其实我觉得“委托”这个词起初理解起来真是有点让人抓耳挠腮的,想一下,你第一眼看到这个词什么感觉?能一下子就想到它是做函数调用的吗,能感觉的到它和函数指针有渊源吗?记得孟老大在一篇文章中谈到过“指针”这个词有误引理解思路之嫌,嘿嘿!有点跑题了。
指针的本质我们都知道,就是其所指向成员的内存地址,函数指针就是要调用函数的内存地址了,地址就是赤裸裸的地址,除了这一地址信息之外其它的一无所有,如关于参数和返回值等的数据类型信息,而委托就不同,委托并不单纯的是调用函数的地址,它本质上是一个类,经过对函数指针的这么一类型化的封装,包含进了数据类型等很多信息,因此委托
被称之为类型安全的指针。青出于蓝胜于蓝,委托还有比函数指针强大的地方,回头看一下前面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两个类,再加上编译器的从中配合,三者很好的演绎了委托这一强大的技术,虽然我们再平时的编成中对他们接触的可能不多,但我感觉认识一下他们也是不错的,至少能加深对委托的理解。
对于学习C/C++语言的朋友,函数指针相信都不会觉得遥远和陌生吧,不管你对它是恨是爱,还是它令你忧伤令你逍遥自在;如果现今你还在使用C/C++,驰骋在Win32平台下怡然自乐,那么有它相伴身边你肯定不会觉得寂寞;如果你已告别C/C++,徜徉在.NET平台下悠然自得,那么没有了它相伴你也肯定不会觉得失落,因为一个名曰委托(Delegete)的朋友已翩然来到我们身边,所以我们用不着遗憾,更用不着千年一叹,因为委托可以称得上是函数指针的改头换面。
地球人都知道,函数指针的主要作用就是用它来调用函数的,那一个函数好端端的为什么不用人家的名字直接调用,偏偏的拐个弯用指针来调用呢?当然普通的调用直接用名字就可以了,但当我们想把函数作为参数来传递,就得靠函数指针;还有在程序界大名鼎鼎的回调函数(CallBack Function),它风光的背后也是函数指针在默默奉献。那么在.NET托管环境下,微软狠心割舍了函数指针这一不求名利的功臣,但没让我们失望的是增添了委托这一良将,把函数指针的功能发挥的淋漓尽致。还是来回忆一下我们的老朋友函数指针,正好这几天我也正在实行一惊天计划,准备对我暗恋已久的一女孩说出心里话,无奈我没经验又害羞,多次欲言却又口难开,所以准备托一好友帮忙先给她点暗示,完了我再随后出击,借写本文机会来场纸上谈兵。首先用函数指针模拟一下这个计划,妥当与否,姑且不作理论。
class liyufeng
{
//注意,此处必须声明为静态方法。
public:
static void mySay()
{
//我准备对女孩说的话,苦思冥想不知作何言
语,各位有经验的朋友请指点一二,万分感激
printf("I Like You,Do you know,if you
don’t know,now I must tell you ,tell you!");
}
};
上面这个类用我大名的拼音来命名,算是代表我这个无缘爱情的苦命人。我要说的话由静态方法mySay来说出,此处只是输出到控制台,要是真能对人家说了就高兴死俺了。
classgoodfriend
{
//这里的声明就是我们熟悉的函数指针了,我说话的那个mySay方法就由这个函数指针代劳,传递给朋友的friendSay方法.
typedef void (*middleSay)();
//这里利用函数指针把函数作为参数传递,朋友的friendSay方法说话,就是函数指针middleSay说话,到最后其实就是我自己的mySay方法说话
public:
static void friendSay(middleSay say)
{
say();
}
};
上面的这个类代表我的好朋友,姓名就不透露了,免得太多的像我这样的不敢对女孩表白的人都去找他这个爱管闲事的家伙,哦,他可就忙不过来啦。我的话呢通过这里的friendSay方法逐句逐字的说给女孩听。朋友代表我和女孩见面的地点定在一个美丽的公园里。
classbeautifulpark
{
//注意:如果在Visual2005里建立一个Win32的控制台程序的话,主函数是不应该封装在类里的,在此只是为了说明和同下边的委托版本保持一致的需要。嘿嘿!总得找个约会地点嘛;运行本程序的话,下边主函数中的内容直接拷贝即可
int _tmain(int argc, _TCHAR* argv[])
{
int c;
//把俺说话的mySay方法传递给朋友的friendSay方法,关键是这个函数指针哦,朋友的friendSay方法才能接受俺的mySay方法(其实关键是一顿美味可口的饭菜)
goodfriend::friendSay(liyufeng::mySay);
scanf_s("%d",&c);
return 0;
}
}
我和朋友一起先到公园,小心翼翼的把我说话的那个方法mySay给了朋友的friendSay方法,为什么要小心?函数指针嘛,不是个省心的东西,当然得小心喽。好了,后边的事情我就不得而知了,天知道女孩听了俺的话作何反应。
我的话说完了,同时和大家一起回忆了函数指针,没经历过亘古悠香的C/C++语言而是直接和.NET下语言做亲密接触的朋友,看了上面的几行代码有没有感觉似曾相识?没有的话,下面我再来模拟一下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!");
}
}
这个liyufeng类和上面的C++版本的不同的只是输出到控制台用的方法不一样。
class goodfriend
{
//这里使用delegate关键字声明.NET下的委托类型,同样,我说话的那个mySay方法就由这个委托代劳,传递给朋友的friendSay方法
public delegate void middleSay();
//这里利用委托把函数作为参数传递,朋友的friendSay方法说话,就是委托middleSay说话,到最后其实就是我自己的mySay方法说话
public static void friendSay(middleSay say)
{
say();
}
}
我的好朋友goodfriend类和前边的C++版本非常相似,唯一不同的是指针换成了委托,但完成的是同样的功能。
class beautifulpark
{
static void Main(string[] args)
{
//把俺说话的mySay方法传递给朋友的friendSay方法,关键是这个委托哦,朋友的friendSay方法才能接受俺的mySay方法(其实关键还是一顿美味可口的饭菜)
goodfriend.friendSay(liyufeng.mySay);
System.Console.Read();
}
}
好了,我追求缘分的计划也用委托班门弄斧的模拟了一番,到此为止,熟悉函数指针还没接触过委托的朋友,还有熟悉委托却对函数指针陌生的朋友,回头看一下,函数指针和委托是否颇为相似?在我初次接触委托的时候,也是被这个家伙搞的糊里糊涂,后来明白了,在.NET托管环境下的相关语言都是以面向对象作为基础来构建设计,一切都是对象,那么在这个对象丛生的世界里,摆脱对象这件外衣的成员是不允许存在的,也就是所有的成员都是从对象的老祖先Object派生而来。指针作为C/C++中一种特殊的数据类型,如果想在托管环境下生存的话,必然也是Object的后代,所谓入乡随俗,自然的也得披上对象的行套;所以对于函数指针而言,便是以一个对象封装了我们想要调用函数的所有相关信息,可以肯定这其中包含我们要调用函数的地址信息。当然C#等语言也只是对函数指针进行了类型化,给它改了个头,换了个面,起了个名叫Delegate,中文唤作委托;而对于其它数据类型的指针,并没有这么做,因此也就意味着我们是不可以像在C/C++中那样,自由自在的随意操作内存,这都是出于安全性的考虑。其实我觉得“委托”这个词起初理解起来真是有点让人抓耳挠腮的,想一下,你第一眼看到这个词什么感觉?能一下子就想到它是做函数调用的吗,能感觉的到它和函数指针有渊源吗?记得孟老大在一篇文章中谈到过“指针”这个词有误引理解思路之嫌,嘿嘿!有点跑题了。
指针的本质我们都知道,就是其所指向成员的内存地址,函数指针就是要调用函数的内存地址了,地址就是赤裸裸的地址,除了这一地址信息之外其它的一无所有,如关于参数和返回值等的数据类型信息,而委托就不同,委托并不单纯的是调用函数的地址,它本质上是一个类,经过对函数指针的这么一类型化的封装,包含进了数据类型等很多信息,因此委托
被称之为类型安全的指针。青出于蓝胜于蓝,委托还有比函数指针强大的地方,回头看一下前面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两个类,再加上编译器的从中配合,三者很好的演绎了委托这一强大的技术,虽然我们再平时的编成中对他们接触的可能不多,但我感觉认识一下他们也是不错的,至少能加深对委托的理解。