引言

上篇.NET (C#) Internals: Delegates (1)我们介绍了委托初识、委托本质、委托的实例化方式、协变委托与逆协变委托,本篇将介绍如下主题:

  1. 1、委托链直观
  2. 2、委托链的本质
    1. 2.1、+=操作
    2. 2.2、-=操作
  3. 3、委托链结构
  4. 4、委托链的返回值问题

1、委托链直观

在上篇中我们知道调用委托,我们只需要简单地以函数调用的方法,如上篇的例子中cb(“skynet”,23)调用委托cb,实际上调用的是PersonInfo方法。因为我们将PersonInfo方法与委托cb绑定了,且委托只绑定了一个方法。大家应该见过类似于下面的代码(注意其中的+=、-=):

  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4.  
  5. namespace DelegateTest  
  6. {  
  7.     class Program  
  8.     {  
  9.         public delegate void CallBack(string name, int number);  
  10.         void PersonInfo(string name, int no)  
  11.         {  
  12.             System.Console.WriteLine(name);  
  13.             System.Console.WriteLine(no);  
  14.         }  
  15.         void PersonInfo1(string name, int no)  
  16.         {  
  17.             System.Console.WriteLine("Person's name is: "+name);  
  18.             System.Console.WriteLine("Person's number is: "+no);  
  19.         }  
  20.  
  21.         static void Main(string[] args)  
  22.         {  
  23.             Program pr = new Program();  
  24.             CallBack cb = pr.PersonInfo;  
  25.             cb += pr.PersonInfo1;  
  26.             cb("skynet", 23);  
  27.             System.Console.WriteLine("-----------------------");  
  28.             cb -= pr.PersonInfo;  
  29.             cb("skynet", 23);  
  30.         }  
  31.     }  

运行上述代码会有如下结果:

C#Internals_Delegates2

图1、委托链实例

从结果来看,我们知道第一次调用cb(“skynet”,23)时,调用了两个方法PersonInfo、PersonInfo1;第二次调用cb(“skynet”,23)时,只调用了PersonInfo1。从源码我们可以看出是什么造成了这样的输出结果:

第一次调用cb(“skynet”,23)跟我们上篇中第一节委托初识中的代码相比只是多了cb += pr.PersonInfo1及相应的PersonInfo1方法,这是造成委托调用了两个方法的原因所在,+=操作将PersonInfo1方法追加到委托使其绑定两个方法,而且调用时是按照绑定的顺序调用方法的(试试先绑定PersonInfo1,再+=PersonInfo,然后调用cb(“skynet”,23)就知道了!)。

第二次调用cb(“skynet”,23)是在cb -= pr.PersonInfo,结果委托知道了PersonInfo1方法,说明-=操作移除了PersonInfo方法与委托cb的绑定。

总之,我们可以知道+=、-=操作分别可以向委托追加绑定方法、移除指定方法与委托的绑定。

2、委托链本质

我们在用+=、-=操作时,编译器为我们做了什么呢?让我们用ILDasm查看刚才生成的DelegateTest可执行文件,如下图:

C#Internals_Delegates2

图2、ILDasm查看DelegateTest可执行文件

其中的Main方法的IL代码如下所示:

  1. .method private hidebysig static void  Main(string[] args) cil managed  
  2. {  
  3.   .entrypoint  
  4.   // Code size       108 (0x6c)  
  5.   .maxstack  4  
  6.   .locals init ([0] class DelegateTest.Program pr,  
  7.            [1] class DelegateTest.Program/CallBack cb)  
  8.   IL_0000:  nop  
  9.   IL_0001:  newobj     instance void DelegateTest.Program::.ctor()  
  10.   IL_0006:  stloc.0  
  11.   IL_0007:  ldloc.0  
  12.   IL_0008:  ldftn      instance void DelegateTest.Program::PersonInfo(string,  
  13.                                                                       int32)  
  14.   IL_000e:  newobj     instance void DelegateTest.Program/CallBack::.ctor(object,  
  15.                                                                           native int)  
  16.   IL_0013:  stloc.1  
  17.   IL_0014:  ldloc.1  
  18.   IL_0015:  ldloc.0  
  19.   IL_0016:  ldftn      instance void DelegateTest.Program::PersonInfo1(string,  
  20.                                                                        int32)  
  21.   IL_001c:  newobj     instance void DelegateTest.Program/CallBack::.ctor(object,  
  22.                                                                           native int)  
  23.   IL_0021:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,  
  24.                                                                                           class [mscorlib]System.Delegate)  
  25.   IL_0026:  castclass  DelegateTest.Program/CallBack  
  26.   IL_002b:  stloc.1  
  27.   IL_002c:  ldloc.1  
  28.   IL_002d:  ldstr      "skynet" 
  29.   IL_0032:  ldc.i4.s   23  
  30.   IL_0034:  callvirt   instance void DelegateTest.Program/CallBack::Invoke(string,  
  31.                                                                            int32)  
  32.   IL_0039:  nop  
  33.   IL_003a:  ldstr      "-----------------------" 
  34.   IL_003f:  call       void [mscorlib]System.Console::WriteLine(string)  
  35.   IL_0044:  nop  
  36.   IL_0045:  ldloc.1  
  37.   IL_0046:  ldloc.0  
  38.   IL_0047:  ldftn      instance void DelegateTest.Program::PersonInfo(string,  
  39.                                                                       int32)  
  40.   IL_004d:  newobj     instance void DelegateTest.Program/CallBack::.ctor(object,  
  41.                                                                           native int)  
  42.   IL_0052:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Remove(class [mscorlib]System.Delegate,  
  43.                                                                                          class [mscorlib]System.Delegate)  
  44.   IL_0057:  castclass  DelegateTest.Program/CallBack  
  45.   IL_005c:  stloc.1  
  46.   IL_005d:  ldloc.1  
  47.   IL_005e:  ldstr      "skynet" 
  48.   IL_0063:  ldc.i4.s   23  
  49.   IL_0065:  callvirt   instance void DelegateTest.Program/CallBack::Invoke(string,  
  50.                                                                            int32)  
  51.   IL_006a:  nop  
  52.   IL_006b:  ret  
  53. // end of method Program::Main 

2.1、+=操作

Main方法的IL代码IL_0021:  call     class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,  class [mscorlib]System.Delegate) 对应的就是+=操作的IL代码。+=实际上是调用了System.Delegate类的Combine(Delegate,Delegate)方法,参数为两个委托对象。之所以能调用System.Delegate类的方法,是因为System.MulticastDelegate类继承自它,而我们所有的委托都继承自System.MulticastDelegate类。事实上,如果你查MSDN你会发现System.Delegate类的Combine是重载的,共有两个重载,如下所示:

  1. Delegate.Combine (Delegate[])
    将委托数组的调用列表连接在一起。

    如果 delegates 数组包含为空引用(在 Visual Basic 中为 Nothing) 的项,则将忽略这些项。调用列表中可包含重复项目,即引用同一对象上同一种方法的项目。

  2. Delegate.Combine (Delegate, Delegate)
    将两个委托的调用列表连接在一起。由 .NET Compact Framework 支持。 调用列表中可包含重复项目,即引用同一对象上同一种方法的项目。注意:第一个参数的委托会被先调,第二个参数的委托会被后调用。

2.2、-=操作

Main方法的IL代码中,-=对应IL_0052:  call         class [mscorlib]System.Delegate [mscorlib]System.Delegate::Remove(class [mscorlib]System.Delegate,class [mscorlib]System.Delegate)。即-=操作实际上是调用了System.Delegate类的Remove(Delegate source,Delegate value)方法。source——将从中移除 value 的调用列表,value——它提供将从其中移除 source 的调用列表的调用列表。该方法的返回值是一个新委托,其调用列表的构成方法为:获取 source 的调用列表,如果在 source 的调用列表中找到了 value 的调用列表,则从中移除 value 的最后一个调用列表。如果 value 为 空引用(在 Visual Basic 中为 Nothing),或在 source 的调用列表中没有找到 value 的调用列表,则返回 source。如果 value 的调用列表等于 source 的调用列表,或 source 为空引用,则返回空引用。注意:如果 value 的调用列表在 source 的调用列表中多次出现,则移除最后一个匹配项。就是移除后绑定的调用列表,运行下面代码就可以得出比较清晰的理解:

  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4.  
  5. namespace DelegateTest  
  6. {  
  7.     class Program  
  8.     {  
  9.         public delegate void CallBack(string name, int number);  
  10.         void PersonInfo(string name, int no)  
  11.         {  
  12.             System.Console.WriteLine(name);  
  13.             System.Console.WriteLine(no);  
  14.         }  
  15.         void PersonInfo1(string name, int no)  
  16.         {  
  17.             System.Console.WriteLine("Person1's name is: "+name);  
  18.             System.Console.WriteLine("Person1's number is: "+no);  
  19.         }  
  20.         void PersonInfo2(string name, int no)  
  21.         {  
  22.             System.Console.WriteLine("Person2's name is: " + name);  
  23.             System.Console.WriteLine("Person2's number is: " + no);  
  24.         }  
  25.         static void Main(string[] args)  
  26.         {  
  27.             Program pr = new Program();  
  28.             CallBack cb = pr.PersonInfo;  
  29.             cb += pr.PersonInfo1;  
  30.             cb += pr.PersonInfo2;  
  31.             cb += pr.PersonInfo;  
  32.             cb("skynet", 23);  
  33.             System.Console.WriteLine("-----------------------");  
  34.             cb -= pr.PersonInfo;  
  35.             cb("skynet", 23);  
  36.         }  
  37.     }  

如果你想移除匹配的所有调用列表的话,请使用RemoveAll方法。

public static Delegate RemoveAll ( Delegate source, Delegate value ):如果 value 的调用列表与 source 的调用列表中一组相邻的元素相匹配,则认为 source 的调用列表内存在 value 的调用列表。如果 value 的调用列表在 source 的调用列表中多次出现,则移除所有调用列表。

3、委托链结构

上篇中的委托本质中知道,任何委托都继承自[mscorlib]System.MutlicastDelegate,且其中有一个类型为System.MutlicastDelegate的私有字段_prev,它指向另一个委托对象,通常为空。没错就是这个字段使我们能将多个委托组成一个链成为了可能,此时除了链头_prev都不为空!_prev字段指向前一个委托对象,所以上面例子中的PersonInfo和PersonInfo1组成的委托链可表示为:

C#Internals_Delegates2

图3、委托链

如果再+=绑定一个PersonInfo2方法,将在PersonInfo2的_prev字段指向PersonInfo1,链上的其他内容不变。那我们再来分析一下当绑定多个方法时,Invoke方法到底是怎么执行的呢?它内部代码大概逻辑如下:

  1. public void Invoke(string,int)  
  2. {  
  3.   if (_prve != null) _prve.Invoke(string ,int);  //如果当前节点的_prev字段不空,就递归调用_prev指向的节点的Invoke方法,直至调用最先绑定的方法。  
  4.  
  5.   _target.method(string ,int);  //最后调用最后绑定的方法  

其实完全可以把我们在数据结构中学的链表知识运用在此来理解委托,或者说委托链就是一个链表。

4、委托链的返回值问题

上面的委托链例子是委托的返回类型为void的情况,想象一下如果委托的返回类型不空,它是会返回每个委托调用的返回值,还是?让我们看看下面这段代码:

  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4.  
  5. namespace DelegateTest  
  6. {  
  7.     class Program  
  8.     {  
  9.         public delegate string CallBack(string name, int number);  
  10.          
  11.         string PersonInfo1(string name, int no)  
  12.         {  
  13.             System.Console.WriteLine("调用了PersonInfo1");  
  14.             return "Person1's name is " + name + ",number is " + no;  
  15.         }  
  16.         string PersonInfo2(string name, int no)  
  17.         {  
  18.             System.Console.WriteLine("调用了PersonInfo2");  
  19.             return "Person2's name is " + name + ",number is " + no;  
  20.         }  
  21.         static void Main(string[] args)  
  22.         {  
  23.             Program pr = new Program();              
  24.             CallBack cb = pr.PersonInfo1;  
  25.             cb += pr.PersonInfo2;              
  26.               
  27.             System.Console.WriteLine("最终返回结果: "+cb("skynet",23));  
  28.         }  
  29.     }  

我们定义的委托的返回类型为string类型,委托实例cb与PersonInfo1和PersonInfo2绑定,运行结果如下图所示:

C#Internals_Delegates2

图4、委托链的返回值

从结果来看我们知道:调用cb("skynet",23)时,PersonInfo1和PersonInfo2都被调用了,但只返回了PersonInfo2的返回值。 可见调用委托链时,只返回最后一个的委托调用!如果我们想返回每个委托调用的返回值,我们该怎么做呢?

设想一下如下情况:我们调用cb("skynet",23)时,它只会返回PersonInfo2的返回值,那我们可不可以获取委托链中绑定的每个方法,然后我们设计自己的调用规则,每次调用绑定方法时将其返回值保持在一个数组里再返回呢?

System.MutlicastDelegate中有一个public sealed override Delegate[] GetInvocationList()方法,它就是返回委托链的调用列表的,它的代码如下:

  1. public sealed override Delegate[] GetInvocationList()  
  2. {  
  3.     object[] objArray = this._invocationList as object[];  
  4.     if (objArray == null)  
  5.     {  
  6.         return new Delegate[] { this };  
  7.     }  
  8.     int num = (intthis._invocationCount;  
  9.     Delegate[] delegateArray = new Delegate[num];  
  10.     for (int i = 0; i < num; i++)  
  11.     {  
  12.         delegateArray[i] = (Delegate) objArray[i];  
  13.     }  
  14.     return delegateArray;  

安装上面的思路我们写出如下代码:

  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4.  
  5. namespace DelegateTest  
  6. {  
  7.     class Program  
  8.     {  
  9.         public delegate string CallBack(string name, int number);  
  10.          
  11.         string PersonInfo1(string name, int no)  
  12.         {  
  13.             System.Console.WriteLine("调用了PersonInfo1");  
  14.             return "Person1's name is " + name + ",number is " + no;  
  15.         }  
  16.         string PersonInfo2(string name, int no)  
  17.         {  
  18.             System.Console.WriteLine("调用了PersonInfo2");  
  19.             return "Person2's name is " + name + ",number is " + no;  
  20.         }  
  21.         static void Main(string[] args)  
  22.         {  
  23.             Program pr = new Program();              
  24.             CallBack cb = pr.PersonInfo1;  
  25.             cb += pr.PersonInfo2;   
  26.               
  27.             StringBuilder strs = new StringBuilder();  
  28.             Delegate[] arrayDelegates = cb.GetInvocationList();  
  29.             foreach (CallBack d in arrayDelegates)  
  30.             {  
  31.                 strs.AppendFormat("{0}{1}",d("skynet",23),Environment.NewLine);  
  32.             }  
  33.             System.Console.WriteLine("最终返回结果:");  
  34.             System.Console.WriteLine(strs.ToString());  
  35.         }  
  36.     }  

运行结果如下图所示:

C#Internals_Delegates2

图5、返回每个委托调用的返回值

从运行结果可以看出,我们思路是可行的,的确达到了返回每个委托调用的返回值的目的。