代码改变世界

.NET (C#) Internals: Delegates (1)

2010-04-09 19:31  吴秦  阅读(3197)  评论(4编辑  收藏  举报

引言

委托(delegate),这个概念大家应该都知道或许还有一些新人不知道,比如说我就是现在才对delegate有个比较清晰的认识,这里我将深入解析delegate跟大家分享我所知道的,希望能对部分人有所帮助,给大家带来不一样的视角。

本文涉及主题如下:

  • 1、委托初识
  • 2、委托本质
    • 2.1、委托类
    • 2.2、委托构造器
    • 2.3、委托调用
  • 3、实例化委托的几种方式
    • 3.1、使用new操作符实例化委托
    • 3.2、用方法名实例化委托
    • 3.3、用匿名方法实例化委托
    • 3.4、Lambda表达式实例化委托
  • 4、协变委托与逆协变委托
    • 4.1、协变委托
    • 4.2、逆协变委托

由于文章比较长,我分为几部分来写,而且文章太长了看起来也比较累。接下来的一篇讲讨论委托链等内容。

1、委托初识

我们知道委托是一个引用类型,所以他具有引用类型所具有的通性。他保存的不是实际值,而是保存对存储在托管堆(managed heap)中的对象的引用。那他保存的是对什么的引用呢?委托保存的是对函数(function)的引用。

对学过C/C++的人,是不是觉得跟函数指针很像呢!其实他们是有区别的,在非托管C/C++中,函数的地址就是一个内存地址。该地址不会携带任何额外的信息,例如函数期望的参数个数、参数类型、函数的返回值类型及函数的调用约定。总之,非托管C/C++中函数指针是非类型安全的。而.NET中的委托是类型安全的,委托会检测他所保存的函数引用是否和声明的委托匹配。下面的代码展示这个:

代码

编译它你将会看到如下错误:

image 图1、证明委托是类型安全的

而如果你的代码如下,将会正确调用PersonInfo函数而不会报错:

代码

 

Note:与C/C++中的函数指针不同,委托是类型安全的,这点很重要!只有跟委托签名相同的方法才能传给/赋给委托。

2、委托本质

在C#中使用delegate关键字定义委托,然后使用我们熟悉的函数调用的语法来调用委托,如上述例子中的cb(“skynet”,23)。在这简单的表象背后,.NET编译器为我们做了什么呢?我们使用ILDasm.exe查看我们上面生成的DelegateTest的exe文件(不报错的那个),如下所示:

image

图2、ILDasm查看DelegateTest.exe

可以知道定义CallBack委托时,编译器为我们做了如下工作,实际上定义任何委托编译器都会做如下工作:

  1. 声明一个类,对应上图中的.class nested public auto ansi sealed。
  2. 该类扩展自System.MutlicastDelegate,对应上图中的extends [mscorlib]System.MutlicastDelegate。
  3. 该类包含一个构造器,对应上图中的.ctor: void(object ,native int)。
  4. 该类包含三个方法,分别是BeginInvoke、EndInvoke、Invoke。

2.1、委托类

当我们用delegate关键字声明委托时,编译器自动为我们生成如图2所示的类。类的名字即为委托变量名,访问类型为定义的委托访问类型。如上例中,public delegate void CallBack(string name, int number);定义的委托对应的类为CallBack,访问类型为public,该类继承自[mscorlib]System.MutlicastDelegate。如果我们定义委托的访问类型为private或者protected,则对应的委托类的访问类型为private或者protected。但是任何委托都继承自[mscorlib]System.MutlicastDelegate。

Note:mscorlib.dll一开始是Microsoft Common Object Runtime Library(微软通用对象运行时库)的首字母缩写。但是当ECMA开始标准化CLR以及部分FCL时,mscorlib.dll正式成为Multilanguage Standard Common Object Runtime Library(多语言标准通用对象运行时库)的首字母缩写。

MulticastDelegate 拥有一个带有链接的委托列表,该列表称为调用列表,它包含一个或多个元素。在调用多路广播委托时,将按照调用列表中的委托出现的顺序来同步调用这些委托。如果在该列表的执行过程中发生错误,则会引发异常。关于委托链的详细讨论将在本文后面讨论。MulticastDelegate类有如下重要的三个私有字段:(关于MulticastDelegate类,想了解更多

字段 类型 描述
_target System.Object 获取委托所表示的方法(继承自 Delegate)。指向回调函数被调用时应该被操作的对象,该字段用于实例化方法的回调。
_methodPtr System.Int32 获取类实例,当前委托将对其调用实例方法(继承自 Delegate)。其主要用于表示指针或句柄,CLR用它来标识回调方法。
_prev System.MulticastDelegate 指向另一个委托对象,该字段通常为null。

现在我们明白了——委托本质上是一个类,所以一个类可以在哪定义,一个委托也就可以在哪定义。

2.2、委托构造器

从图2还可以看出委托类包含一个构造器,并且构造器接受两个参数:一个对象引用一个指向回调函数方法的整数。即,分别对应着2.1中所提到的MulticastDelegate类的_target、_methodPtr字段。事实上,MulticastDelegate类的构造器有三个重载,如下:

  1. 2h7wdx6c.protmethod(zh-cn,VS.90).gif2h7wdx6c.CFW(zh-cn,VS.90).gifMulticastDelegate()注意:仅.NET Compact Framework 2.0中支持,后面的版本3.5已经移除了它
  2. 2h7wdx6c.protmethod(zh-cn,VS.90).gifMulticastDelegate(Object target, String method):target——在其上定义 method 的对象,method——为其创建委托的方法的名称。此构造函数从编译器生成的代码所产生的类中调用。
  3. 2h7wdx6c.protmethod(zh-cn,VS.90).gifMulticastDelegate(Type target, String method):target——在其上定义 method 的对象的类型,method——为其创建委托的静态方法的名称。此构造函数是从某个类中调用的,它根据一个静态方法名称以及定义该方法的类的 Type 来生成一个委托。

上例中,语句CallBack cb = pr.PersonInfo; 就是调用的MulticastDelegate(Object target, String method)方法实例化的委托。

从构造器也可以看出,每个委托对象实际上是对方法其调用时操作的对象的一个封装。MulticastDelegate类定义了两个只读公有实例属性:TargetMethod。给定一个委托对象的引用,我们可以查询这些属性。Target属性返回一个方法回调时操作的对象引用。如果是静态方法,Target将返回null。Method属性返回一个标识回调方法的System.Reflection.MethodInfo对象。

2.3、委托调用

前面说了如何声明委托并用委托构造器实例化,那如何来调用委托呢?我们先来看看上例中main函数的IL代码,如下图所示:

委托调用

图3、main函数IL代码

从图3可以知道,Main函数:1、调用类Program的构造器实例化Program对象;2、实例化Program的PersonInfo方法;3、调用委托CallBack的构造器,参数为object、int,即调用的是2.2中所讲的第二个构造器;4、加载“skynet”,23作为委托的参数,调用委托。

从Main函数的第4步可以知道实际上是通过Invoke(string,int32)方法调用委托,但注意C#中我们不可以通过Invoke方法显示地调用委托。当Invoke被调用时,它使用_target和_methodPtr两个私有字段来在指定的对象上调用期望的方法。注意Invoke方法的签名和CallBack委托的签名是相匹配的。换句话说,CallBack接受2两个参数且返回void,所以Invoke方法也接受同样的2个参数且返回void。

事实上,.NET Framework 允许您异步调用任何方法。为此,应定义与您要调用的方法具有相同签名的委托;CLR会自动使用适当的签名为该委托定义 BeginInvoke 和 EndInvoke 方法。说明:.NET Compact Framework 中不支持异步委托调用,也就是 BeginInvoke 和 EndInvoke 方法。

BeginInvoke 方法启动异步调用。该方法与您需要异步执行的方法具有相同的参数,还有另外两个可选参数。第一个参数是一个 AsyncCallback 委托,该委托引用在异步调用完成时要调用的方法。第二个参数是一个用户定义的对象,该对象将信息传入回调方法。BeginInvoke 会立即返回,而不等待异步调用完成。BeginInvoke 返回一个可用于监视异步调用进度的 IAsyncResult

EndInvoke 方法检索异步调用的结果。在调用 BeginInvoke 之后随时可以调用该方法。如果异步调用尚未完成,则 EndInvoke 会一直阻止调用线程,直到异步调用完成。EndInvoke 的参数包括您需要异步执行的方法的 out 和 ref 参数(在 Visual Basic 中为 <Out> ByRef 和 ByRef)以及由 BeginInvoke 返回的 IAsyncResult

代码示例(查看)演示了使用 BeginInvoke 和 EndInvoke 进行异步调用的四种常用方法。调用 BeginInvoke 之后,您可以执行下列操作:

  • 进行某些操作,然后调用 EndInvoke 一直阻止到调用完成。
  • 使用 IAsyncResult.AsyncWaitHandle 属性获取 WaitHandle,使用其 WaitOne 方法一直阻止执行直到发出 WaitHandle 信号,然后调用 EndInvoke。
  • 轮询由 BeginInvoke 返回的 IAsyncResult,以确定异步调用何时完成,然后调用 EndInvoke。
  • 将用于回调方法的委托传递给 BeginInvoke。异步调用完成后,将在 ThreadPool 线程上执行该方法。回调方法调用 EndInvoke。

3、实例化委托的几种方式

委托虽然是引用类型,也具有引用类型的通性——保存的是托管堆中对象的引用,但是delegate也具有独特之处,除了用new操作符实例化之外,还有用其他几种方法实例化。

3.1、使用new操作符实例化委托

跟普通类一样,可以使用new操作符实例化委托,如下代码所示:

代码

 

值得注意的是,new操作符实例化委托时传的参数的一个方法,如上代码所示CallBack cb = new CallBack(pr.PersonInfo)。然而,实际上编译器知道我们正在构造一个委托,它会通过分析源代码来确定我们引用的是哪个对象和方法。其中的对象引用会被传递给target参数,一个特殊的标识方法的Int32值(由MehtodDef或者MethodRef元数据标记获得)会被传递给methodPtr参数。对于静态方法而言,null会被传递给target参数。在构造器内部,这两个参数会被保存在相应的私有字段中。

委托除了调用实例方法还可以引用静态方法,假如上述示例中PersonInfo方法是静态的,则只需这样调用而不需要先实例一个Program对象:

代码

对于委托调用静态方法同样适用于后面的几种实例化委托方法。

3.2、用方法名实例化委托

如第一节委托初识中给出的代码就是使用这种方法,这里就不累述了。这种方法相对于匿名委托(见3.3)叫做有名委托。

3.3、用匿名方法实例化委托

用匿名方法实例化委托,即将匿名方法赋给委托。注意:匿名方法中的变量的生命周期将扩展到委托的生命周期。代码示例如下:

代码

用ILDasm查看匿名方法实例化委托生成的IL代码,可知本质跟有名方法一样,如下图所示:

image 图4、匿名方法实例化委托

 

3.4、Lambda表达式实例化委托

这是C# 3.0中引入的,Lambda表达式是函数编程(Functional Programming)的核心概念,关于Lambda请自行查阅相关资料。代码示例:

代码

4、协变委托与逆协变委托

在第一个委托初识中我们知道了:委托是类型安全的,只有方法的签名和委托的签名相同时,方法才能传给/赋给委托。但是协变和逆协变为我们提供了一定程度的灵活性。协变允许方法具有的派生返回类型比委托中定义的更多。逆变允许方法具有的派生参数类型比委托类型中的更少。即,委托中的协变只要针对方法及委托的返回值类型而言,而逆变则针对方法及委托中的参数而言。

4.1、协变委托

当委托方法的返回类型具有的派生程序比委托签名更大时,就称为协变委托方法。因为方法的返回类型比委托签名的返回类型更具体,所以可对其进行隐式转换,这样该方法就可用作委托。协变使得创建可被类和派生类同时使用的委托方法成为可能。代码示例:

代码

4.2、逆协变委托

 

当委托方法签名具有一个或多个参数,并且这些参数的类型派生自方法参数的类型时,就称为逆变委托方法。因为委托方法签名参数比方法参数更具体,因此可在传递给处理程序方法时对他们隐式转换。这样逆变使得大量类使用的更通用的委托方法的创建变得更简单。代码示例:

代码