Da Vinci's CyberSpace

手把青秧插满田, 低头便见水中天; 心地清净方为道, 退步原来是向前.
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

.NET下午茶之四:品味委托

Posted on 2008-05-24 18:20  Da Vinci  阅读(445)  评论(0编辑  收藏  举报

网上讲委托的文章有N多,技术书籍与文档中涉及.NET的也必讲委托,原因很简单:过去没有。这样一个新的point在初学者看起来似乎有些高深莫测,但实际上它是我们过去都使用过的一种技术的近义词,这种技术就是函数指针,.NET用委托来实现类型安全的回调函数机制。委托就是一种类(引用类型),C#的关键词delegate创建了这个类。

   注:委托类的父类是System.MulticastDelegate,我们没办法直接继承它。

委托概览

OK,现在创建一个委托看看:
 1using System;
 2public class dotnet
 3{
 4    public delegate void Test();  //没有参数的委托
 5    public delegate void Test1( Int32 item );
 6
 7    public void foo()
 8    {
 9        Console.Writeline("no parameters");
10    }

11    public int foo1( int item )
12    {
13        return item;
14    }

15}

16public static void Main()
17{
18    Test t = new Test( foo );
19    t();   //调用foo方法,t为委托对象
20    Test1 t1 = new Test1( foo1 );
21    Int32 i = t1( 100 );  //调用 foo1(100)方法,t1为委托对象
22}

似乎不是太复杂,但是编译器和CLR还是隐瞒了我们很多东西(Lippman在《Inside C++ Object Model》中对编译器在背后默默所做的事情表达了他的看法)。那么事实是什么呢?看下面的代码:

public delegate void Dele( Object a, Int32 item, Int32 num );

编译器把这句代码翻译成一个更复杂的类:

 1public class Test : System.MulticastDelegate
 2{
 3    public Test( Object target, Int32 methodPtr); //一个构造器
 4    public void virtual Invoke( Object a, Int32 item, Int32 num );  //一个Invoke方法,其中参数和原委托参数相同
 5       
 6    //委托的异步调用
 7    public virtual IAsyncResult BeginInvoke( Object a, Int32 item, Int32 num, AsynsCallback callback, Object object );
 8    public virtual void EndInvoke( IAsyncResult result);
 9}

10

上面可以看到Test类继承了MulticastDelegate类。在MulticastDelegate类中有一些字段涉及到委托的内部机制:_target字段指示了被回调的对象,_methodPtr是一个整数,用来标志回调方法。在上述Test类中的构造器参数就代表这两个私有字段。另外MulticastDelegate类还有_prev字段指示了另外的一个委托对象,这主要用在委托链表中,下面会再讨论。
在我们构造委托时,构造器把对象的引用传递给target参数,另外在元数据中的MethodDef或MethodRef的标记传递给methodRef。当我们调用一个委托对象时,会调用该对象的Invoke方法。例如在刚才创建的委托对象中,编译器会把t()翻译成t.Invoke()。

匿名委托

委托本身的价值主要体现在事件处理方面,关于事件本篇不想多说,后续主题会讨论。下面来看一下C#2.0里面增加的匿名委托。
匿名委托(确切的说是匿名方法声明委托)就是允许一个与委托关联的代码被内联地写入使用委托的地方,这使得代码对于委托的实例很直接。MSDN:"如果使用匿名方法,则不必创建单独的方法,因此减少了实例化委托所需的编码系统开销"。OK,看看C#1.x中的委托版本:

public delegate void Test();

public void test()
{
    Test test 
= new Test(foo);
    test();
}

public static void foo()
{
    Console.WriteLine("
Anonymous Method");
}

Now, C#2.0的匿名委托版本:

public void test()
{
   Test test 
= delegate() { Console.WriteLine("Anonymous Method"); };
   test();
}

Oh,你说很简单了!至少方法不用写了。不急,先看看它内部是什么原理:


                                        图1. C#1.x 命名委托反编译代码


                                        图2. C#2.0匿名委托反编译代码

上面可以看到,委托类没有任何改变,倒是在Program类里添加了一个新的方法:<Main>b__0:void()。当使用了匿名方法的类里,CLR会自动生成一个跟调用命名方法时有同样签名的EventHandler和一个method来处理,所以,匿名方法只是减少了我们的编码量。在查看IL代码时可以看到,Program类里有一个名为<>9__CachedAnonymousMethodDelegate1的EventHandler和一个名为<Main>b__0的跟EventHandler的签名符合的方法。来看看这个新生成的方法:

.method private hidebysig static void  '<Main>b__0'() cil managed
{
  .custom instance 
void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  
// Code size       13 (0xd)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      
" Anonymous Method "
  IL_0006:  call       
void [mscorlib]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  ret
// end of method Program::'<Main>b__0'

这里实际上就是执行了Console.WriteLine("Anonumous Method")。

    注:关于匿名方法更多深入的主题, 请参考:http://www.microsoft.com/china/msdn/library/langtool/vcsharp/CreElegCodAnymMeth.mspx?mfr=true
                                                          
委托链

委托链技术使委托更有价值,在MulticaseDelegate对象中的_prev字段就是指向了另一个MulticaseDelegate对象的引用。这样委托对象就可以组成一个链表。我们可以利用Delegate类中的Combine方法创建链表,用Remove方法移除链表中的一个委托。

   注:Combine方法有两种形式:委托数组形式和head/tail形式,每种都可以操作委托链表。

例如,操作一个委托链表:
Test test1 = new Test(Fun1); // Fun1, Fun2为调用相应的方法
Test test2 = new Test(Fun2);

//创建一个委托链表
Test test = (Test) Delegate.Combine(test1,test2);

//从委托链表上移除一个委托对象
test = (Test) Delegate.Remove(test, new Test1(Fun1));

在创建委托链表操作中,编译器会判断委托链表的_prev字段是否为空,若不为空,则递归调用链表上的委托对象;在移除委托链表操作中,调用Remove方法时需要创建一个新的委托对象。该委托对象的_target和_methodPtr字段会被初始化。Remove在委托链表中寻找与该委托链表相等的委托对象,如果找到则移除(修正委托链表的_prev字段),最后返回委托链表头。
需要注意的是Invoke方法调用每个委托对象之前的对象,这样虽然每个对象都会调用到,但是实际上最终得到的返回值只是最后一个方法的返回值,在这期间得到的方法的返回值会被丢弃。另外若调用期间发生了异常,则会影响到后续方法的调用。改善这种情况的方法是利用GetInvocationList方法:

public virtual Delegate[] GetInvocationList();

该方法返回一个委托数组,它遍历委托链表上的委托对象,并为拷贝每一个对象到数组中,同时这些数组中的委托对象的_prev字段都会被设为空,这样就达到了相对独立的目的。例如对上述test委托链表:

Delegate[] deleArray = test.GetInvocationList();

foreach( Test t in deleArray )
   
//do something.


委托与设计模式


在设计模式中,也有一种委托机制Delegation(注意Delegation并不是一种模式)。它是一种组合方法,两个对象处理同一个请求,接受请求的对象把操作委托给其代理者(即委托)。类似于子类将其请求交给父类处理。State、Strategy和Visitor模式都使用了委托的机制。可以看出.NET中的委托和Delegation的意思很接近,但在实现中有些不同。.NET委托能更加灵活的实现对象间的解耦,发挥设计模式中委托机制的作用。

关于委托还有更多有意义的话题,比如事件与委托技术、反射技术与委托、委托在实际组件设计中的作用等,会在今后的探索中不断深入讨论。


参考资料: Jeffrey Richter《Applied Microsoft .NET Framework Programming》
Firefox 3