委托、事件、反射、特性等.NET中的高级特性,对这些特性的掌握和熟练运用,往往成为.NET程序员从入门到中级的评价标准。这篇文章DebugLZQ谈一下.NET中的委托特性在.NET框架中是如何实现的,如文章题目说说:浅谈.NET中委托的本质。
委托这一特性对于有过C++编程经验的程序员来说并不陌生,C++中的函数指针和委托确实非常相似,很多人喜欢吧.NET中的委托称为“安全的函数指针”。DebugLZQ这里不去争论这种说法正不正确,但委托确实实现了和函数指针非常相似的功能,那就是程序回调指定方法的机制。
在委托的内部,包含了一个指向某个方法的指针,在这一点上,委托的实现机制和C++的函数指针完全相同。之所以称委托为安全的,是因为委托和其他.NET成员一样,是一种类型,任何委托对象都是System.Delegate的某个派生类的一个对象,在.NET框架中,委托的类结构如下图所示:
从这个结构中可以看出任何自定义的委托都继承自父类System.Delegate。在这个类中,定义了大部分委托的特性,而关于System.MulticastDelegate的特性,后面将介绍。
下面分析一个具体的例子,代码如下:
using System; namespace 委托本质 { class Program { /// <summary> /// DebugLZQ解析委托 /// 定义委托。 /// </summary> /// <param name="i">接受一个整型参数</i> public delegate void TestDelegate(int i); static void Main(string[] args) { //调用委托方法 TestDelegate d = new TestDelegate(PrintMessage1); d(0); d(1); Console.Read(); } /// <summary> /// 一个静态方法,符合TestDelegate的定义 /// </summary> /// <param name="i">整型参数</param> static void PrintMessage1(int i) { Console.WriteLine("第" + i + "个方法"); } } }
在上面代码中,首先通过public delegate void TestDelegate(int i);定义一个名为TestDeletate的新类型,这个类型继承自System.MuticastDelegate。而且它会包含一个名为Invoke的方法,该方法接受一个整型的参数并且没有返回值。这些步骤是由C#编译器自动完成的。
然后声明一个TestDelegate的对象d,并且绑定了一个静态的方法 void PrintMessage1到该委托上。需要注意的是委托可以接受实例方法,也可以接受静态方法,其区别将在下面讲述。最后,也是令人期待的部分,d被调用执行:d(0)、d(1);这里各位可能会产生困惑,事实上,这只是C#设计者为简化程序员的输入而设计的一种语法而已。在本质上,委托的调用就是执行了在定义委托时生成的Invoke方法。
为了容易理解,我们完全可以把委托的调用部分写成如下形式:
d.Invoke(0); d.Invoke(1);
当委托执行时,.NET 检查委托对象并找到PrintMessage1(int i)方法,然后把参数传递给该方法并且执行。
下面是程序的运行结果:
小结:委托是一类继承自System.Delegate的类型,每个委托对象至少包含一个指向某个方法的指针,该方法可以是实例方法,也可以是静态方法。委托实现了回调方法的机制,能够帮助程序员更加简洁优美的设计面向对象程序。
为了进一步弄清楚委托的本质,我们来介绍下委托的内部结构。下面我们先看一下System.Delegate的结构,如下图所示:
_target是一个指向目标实例的引用。当绑定一个实例方法给委托时,该参数会被设置为该方法所在类型的一个实例对象。而当绑定一个静态方法给委托时,该参数会被设置为null。(委托回调实例方法和静态方法的本质区别就在这!)
_methodPtr是一个指向绑定方法代码的指针,和C++中的函数指针及其类似。绑定静态方法或是实例方法在这个成员的设置上并没有不相同。
事实上,对于继承自System.MuticastDelegate的自定义委托来说,还有另外一个成员变量:_prev,该指针指向委托链中的下一个委托,这个将在下面进行介绍。
委托链是一个由委托组成的链表,而不是一个新的东西。从1中的图可以看到,所有的自定义委托都直接集成自System.MulticastDelegate类型,这个类型即是为委托链而设计的。
为了更彻底的理解链式委托的实现机制,有必要来看一下System.MulticastDelegate的内部成员,其重要的三个成员如下图所示:
前面已经讲过System.Delegate的两个内部成员,System.MulticastDelegate继承了这两个成员,并且添加了一个_prev成员,该成员是一个委托的引用变量,当摸个委托被串联到当前委托的后面时,该成员会被设置指向那个后续委托实例对象。.NET就靠这一引用来逐一找到当前委托的所有后续委托并依此执行。
DebugLZQ再次强调,链式委托是指一个委托的链表,而不是指另外一类特殊的委托,当执行链上的一个方法时,后续委托将会被依此执行。如何改变执行顺序,后面将介绍。System.MuticastDelegate定义了对链式委托的支持。在System.Delegate的基础上,它增加了一个指向后续委托的指针,这样就实现了一个简单的链表结构。
为了更深一层的理解链式委托,下面来看一个链式委托的例子:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace 链式委托 { class Program { /// <summary> /// 定义的委托。 /// </summary> public delegate void TestMultiDelegate(); static void Main(string[] args) { //申明一个委托变量,并绑定第一个方法 TestMultiDelegate handler = new TestMultiDelegate(PrintMessage1); //绑定第二个方法 handler += new TestMultiDelegate(PrintMessage2); //绑定第三个方法 handler += new TestMultiDelegate(PrintMessage3); //检查结果 handler(); Console.Read(); } static void PrintMessage1() { Console.WriteLine("第一个方法"); } static void PrintMessage2() { Console.WriteLine("第二个方法"); } static void PrintMessage3() { Console.WriteLine("第三个方法"); } } }
关于以上代码,不做过多的解释。为了便于理解,把上面的代码的核心部分用一种比较复杂的方式进行重写:
TestMultiDelegate handler = new TestMultiDelegate(PrintMessage1); TestMultiDelegate handler2 = new TestMultiDelegate(PrintMessage2); TestMultiDelegate handler3 = new TestMultiDelegate(PrintMessage3); TestMultiDelegate handlerhead = handler + handler2 + handler3; handlerhead.Invoke();
事实上,这两种写法的本质完全一样,只是第一种写法更简洁通用。
程序的输出如下:
从结果可以看出,当程序调用委托链的链头handlerhead时,挂在这个委托之后的所有委托方法都被依此调用了。
我们不妨试下把handlerhead的调用改为如下代码:
handler();
执行之后,会发现执行结果和替换之前一模一样。事实上,上面这段代码没有为handlerhead分配任何委托实例,而仅仅把所有县城的委托串联,并让handlerhead引用到委托链的头上,所以handlerhead和handler实际上引用了同一个委托实例。
链式委托的执行顺序是:按照委托链上的顺醋从当前委托开始依次往后执行,如果有需要可以使用GetInvocationList()方法来获得委托链上所有需要执行的委托,并且按照任何希望的顺序去执行(Invoke)他们。
委托可以是带有返回值的方法,但多余一个带返回值的方法被添加到委托链中时,程序员需要手动地调用委托链上的每个方法,否则委托使用者智能得到委托链上最后一个被执行的方法的返回值。
委托的应用场合通常是任务的执行者把细节工作进行再分配,执行者确切地知道什么工作将要被执行,但却把执行细节委托给其他组件、方法或者程序集。