网上讲委托的文章有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}
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
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");
}
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();
}
{
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'
{
.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));
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.
foreach( Test t in deleArray )
//do something.
委托与设计模式
在设计模式中,也有一种委托机制Delegation(注意Delegation并不是一种模式)。它是一种组合方法,两个对象处理同一个请求,接受请求的对象把操作委托给其代理者(即委托)。类似于子类将其请求交给父类处理。State、Strategy和Visitor模式都使用了委托的机制。可以看出.NET中的委托和Delegation的意思很接近,但在实现中有些不同。.NET委托能更加灵活的实现对象间的解耦,发挥设计模式中委托机制的作用。
关于委托还有更多有意义的话题,比如事件与委托技术、反射技术与委托、委托在实际组件设计中的作用等,会在今后的探索中不断深入讨论。
参考资料: Jeffrey Richter《Applied Microsoft .NET Framework Programming》