委托与事件

摘要

 

委托与事件,这是一个老生常谈的话题,很多人在讲,很多人在用,但似乎它是一个永远也说不完道不尽的东西。那么,到底什么是委托?什么是事件?委托链又是怎么回事?为什么使用事件时常常用到+=/-=?委托又是如何支持协变和逆变的呢?你喜欢使用Action和Func<T,TResult>吗?由于内容比较多,这一章将分上、下两部分慢慢为你讲解。

第一节委托

 

回调函数是Windows编程语言中一种常见而有用的编程实践,在C/C++中,它指的是函数调用的指针,通过这个指针可以方便地对函数进行调用,当然这个指针也是可以被传递给别的函数使用。在.NET Framework中,回调是通过委托来实现的,当然在这里,比起非托管的C/C++,.NET中的委托提供了更丰富的功能,比如同步和异常调用、委托链等等。

 

委托其实是一种类型,是一种定义方法签名的类型,它支持以new的方式来实例化。委托是使用关键字delegate进行定义的,它其实是对方法的包装和聚集。既然它也是一种类型,所以能定义类的地方都可以定义委托,如下是一个委托的声明:

 

public delegate void ShowMessage(stringmsg);

 

任何与委托签名匹配的方法都可以分配给委托,这就要求该方法的返回值类型与参数列表必须与委托的签名相匹配,方法可以是静态的,也可以是对象级的,通过委托可以对分配给委托的方法进行调用。我们来看一下编译器来干了什么事:

 

通过上图我们可以看出,编译器让这个委托类型继承了System.MulticastDelegate类,System.MuticastDelegate类又继承了Delegate类,如下:

 

public abstract class MulticastDelegate :Delegate

{

   //...

}

 

同时还生成了三个方法,其中BeginInvoke()和EndInvoke()两个方法是供异步调用,Invoke()方法是供同步调用。其实通过IL更能明确的看出委托最终经过编译器生成的是一个类:

 

在Delegate类中有两个非常重要的字段:

 

internal object _target; 当委托包装的是一个静态方法时,该字段为null;当委托包装的是一个对象方法时,该字段引用的是该对象。该字段可以通过属性Target获取。

 

internal IntPtr _methodPtr; 保存着一个方法的IntPtr值,属性Method 获取一个标识了该回调方法的对象(MethodInfo)的引用,在类的内部,这个MethodInfo对象是通过方法GetMethodImpl()运算生成来的。

 

我们分别定义一个实例级方法ShowString()和一个静态方法StaticShowString(),代码:

复制代码

 

void ShowString(String str)

       {

           Console.WriteLine("ShowString:" + str);

       }

   public class Code_05_01

    {

       public static void StaticShowString(string str)

       {

           Console.WriteLine("StaticShowString:" + str);

       }

    }

 

复制代码

 

现在我们来看一下运行时的这两个属性,如下图:

 

上图中显示了Target指向的是对象,下图中由于绑定了一个静态方法,所以Target是null。

 

MulticastDelegate类有两个私有字段:

 

private IntPtr _invocationCount; 保存了委托链中方法个数。

 

private object _invocationList; 保存的即是委托链(方法集合)

 

MulticastDelegate类重写了Delegate的一个虚方法publicvirtual Delegate[] GetInvocationList(),来获取委托的调用列表。

 

对委托的调用也有两种方式,可以同步调用也可以异步调用,同步调用有两种方法:直接调用和使用委托对象的Invoke方法,如下:

复制代码

 

void TestCall()

       {

           ShowMessage callShow = new ShowMessage(ShowString);

           callShow("abc");

       }

       void TestInvoke()

       {

           ShowMessage invokeShow = new ShowMessage(ShowString);

           invokeShow.Invoke("abc");

       }

 

复制代码

 

这两种调用有什么区别呢?我们来看一下它们的IL。

 

TestCall.IL:

复制代码

 

.method private hidebysig instancevoid  TestCall() cil managed

{

  // 代码大小       27 (0x1b)

 .maxstack  3

 .locals init ([0] class ConsoleApp.Example05.ShowMessage callShow)

 IL_0000:  nop

 IL_0001:  ldarg.0

 IL_0002:  ldftn      instance voidConsoleApp.Example05.Code_05::ShowString(string)

  IL_0008:  newobj    instance void ConsoleApp.Example05.ShowMessage::.ctor(object,

                                                                            native int)

 IL_000d:  stloc.0

 IL_000e:  ldloc.0

 IL_000f:  ldstr      "abc"

 IL_0014:  callvirt   instance voidConsoleApp.Example05.ShowMessage::Invoke(string)

 IL_0019:  nop

 IL_001a:  ret

} // end of method Code_05::TestCall

 

复制代码

 

TestInvoke.IL:

复制代码

 

.method private hidebysig instancevoid  TestInvoke() cil managed

{

  // 代码大小       27 (0x1b)

 .maxstack  3

 .locals init ([0] class ConsoleApp.Example05.ShowMessage invokeShow)

 IL_0000:  nop

 IL_0001:  ldarg.0

 IL_0002:  ldftn      instance voidConsoleApp.Example05.Code_05::ShowString(string)

 IL_0008:  newobj     instance voidConsoleApp.Example05.ShowMessage::.ctor(object,

                                                                            native int)

 IL_000d:  stloc.0

 IL_000e:  ldloc.0

 IL_000f:  ldstr      "abc"

 IL_0014:  callvirt   instance void ConsoleApp.Example05.ShowMessage::Invoke(string)

 IL_0019:  nop

 IL_001a:  ret

} // end of method Code_05::TestInvoke

 

复制代码

 

其实两者的内部调用实现基本一样,都是对委托对象的方法Invoke进行调用。

 

委托链也叫多路广播委托,是在委托内部由委托对象构成的一个委托对象集合,可以通过委托来调用委托链内的所有委托包装的方法。Delegate有两个静态方法,Combine()用于创建委托链和委托链添加新的委托,我们假设有一个委托委托A,来模拟向委托链追加委托的过程:

 

(1)委托A对象在实例化的时候已经包装了一个方法,

 

(2)当调用Delegate.Combine()方法向委托A追加新委托B对象时,在内部会重新创建一个委托对象C,并且用新追加的委托(方法)初始化_target和_methodPtr字段;

 

(3)将委托C的_invocationList初始化为一个委托对象数组,并将委托A放到这个数组的第1项(索引为0)位置,然后将新的委托B对象放到数据的第2项位置(这里会根据委托的个数依次递增,新增加的那个委托对象总是在这个数组的最后位置),最后返回这个新创建的委托对象C。

 

当再次向新委托C追加委托成员时,会重复(1)-(3)的步骤,每次最终都会返回一个新创建的委托,并且字段_target和_methodPtr总是根据新增加的委托对象来实例化,不过此时的这两个字段好像用处已经不大了,它只是保存了是最后进来的一个委托对象的部分数据。很显然,如果一个委托只包装了一个方法,并没有因追加新的委托而创建委托链,那么在这种情况下,这两个字段_target和_methodPtr是非常有意义的。

 

与方法Combine()对应的有一个方法public static Delegate Remove(Delegate source, Delegate value);很显然它是从委托链中移除一个委托对象。为了方便书写,C#为委托类型的实例重载了两个操作符+=和-=分别对应于方法Combine和方法Remove。通过下图我们可以看一下追加委托的过程。继续对上面的代码进行改造,增加一个对象级方法:

 

void ShowString2(String str)

       {

           Console.WriteLine("ShowString2:" + str);

       }

 

然后实例化一个委托ShowMessage show3 = new ShowMessage(ShowString);,如下图:

 

可以看到此时show3的Method指向的方法是ShowString(System.String),并且字段_invocationCount的值为0,_invocationList是null。

 

接下来我们向show3追加一个委托对象(事实上是向委托链追加),show3 += new ShowMessage(ShowString2);,也可以使用简写:show3 +=ShowString2;这样不用手写代码来创建委托对象,但在编译的过程中,编译器还是会识别出这是一个创建委托对象的过程并向IL中写入创建委托对象的代码。如下图:

 

此时,我们已经看到Method指向的是新的方法ShowString2(System.String),_invocationCount的值为2,_invocationList已经是一个拥有两个元素的集合。

 

对委托对象有委托链且不为空的时候,又是如何调用委托链内的各个回调函数的呢?通过上面对Invoke的讨论,我们知道当调用一个委托对象的回调函数时,在内部CLR实际上是调用了Invoke方法,而在调用invoke方法时,该委托会发现字段_invocationList不为null,接着就会遍历该数组中的所有委托对象依次对委托方法进行调用。

 

协变性与逆变性

 

委托的协变性是指委托方法能返回从对应委托的返回类型派生的一个类型。

 

委托的逆变性是指方法获取的参数类型可以是委托的参数类型的基类。

 

这里的描述的有点绕口,我们来年如下代码:

复制代码

 

public class Code_05_02

    {

       public string Name { get; set; }

    }

   public class Code_05_03 : Code_05_02

    {

       public int Age { get; set; }

    }

//定义一个委托MyDel

public delegate Code_05_02 MyDel(Code_05_03para);

//定义一个与委托MyDel 相匹配的方法

       private Code_05_03 GetData(Code_05_02 para)

       {

           return new Code_05_03();

       }

//以下的实例化及调用是可行的。

           MyDel del = new MyDel(GetData);

           del(new Code_05_03());

 

复制代码

 

Code_05_03类继承于Code_05_02类,委托MyDel的返回类型是Code_05_02,方法GetData的返回类型是Code_05_03,这体现的协变性;方法GetData的参数类型是Code_05_02,委托的参数类型是Code_05_03,这体现了逆变性。

 

需要说明一点的是:协变性和逆变性不能用于值类型(包括void)。

 

在我们开发的过程中,可能经常要使用委托,但委托的定义都大同小异,很幸运的是.NET Framework为我们预定义了很多的常用泛型委托。

 

无返回值的Action系列:

 

public delegate void Action<in T>(Tobj)

public delegate void Action<in T1, inT2>(T1 arg1, T2 arg2)

//共有16个,另外还有一个无参的非泛型委托:

public delegate void Action()

 

使用非常简单,代码示例:

 

void TestAction()

       {

           Action<string> act = new Action<string>(ShowString);

           act("Action");

       }

 

有返回值的Func<T,TResult>系列:

 

public delegate TResult Func<in T, outTResult>(T arg)

public delegate TResult Func<in T1, inT2, out TResult>(T1 arg1, T2 arg2)

//共有16个,另外还有一个无参的泛型委托:

public delegate TResult Func<outTResult>()

 

一般的时候,我们使用这些委托已经足够了。详细内容可以查询MSDN: Action<T>系列和 Func<T,TResult>系列。

 

C#的lambda表达式为委托的简化使用显示出了很不错的编程体验。可以参考MSDN的相关章节:Lambda 表达式(C# 编程指南)。

 

这一章的上半部分,我们主要讲解了与委托相关的内容,后一篇的下半部分将主要讲解什么是事件及委托如何与事件共事。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值