zl程序教程

您现在的位置是:首页 >  后端

当前栏目

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

2023-09-14 08:58:21 时间

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

1、委托初识

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

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

using System;

using System.Collections.Generic;

using System.Text;

namespace DelegateTest

 class Program

 public delegate void CallBack(string name, int number);

 void PersonInfo()

 static void Main(string[] args)

 Program pr = new Program();

 CallBack cb = pr.PersonInfo;

}

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

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

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

using System;

using System.Collections.Generic;

using System.Text;

namespace DelegateTest

 class Program

 public delegate void CallBack(string name, int number);

 void PersonInfo(string name, int no)

 System.Console.WriteLine(name);

 System.Console.WriteLine(no);

 static void Main(string[] args)

 Program pr = new Program();

 CallBack cb = pr.PersonInfo;

 cb("skynet", 23);

}

 

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

2、委托本质

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

image

图2、ILDasm查看DelegateTest.exe

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


该类扩展自System.MutlicastDelegate,对应上图中的extends [mscorlib]System.MutlicastDelegate。
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类,想了解更多


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

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

2.2、委托构造器

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


2h7wdx6c.protmethod(zh-cn,VS.90).gif2h7wdx6c.CFW(zh-cn,VS.90).gifMulticastDelegate(),注意:仅.NET Compact Framework 2.0中支持,后面的版本3.5已经移除了它 2h7wdx6c.protmethod(zh-cn,VS.90).gifMulticastDelegate(Object target, String method):target——在其上定义 method 的对象,method——为其创建委托的方法的名称。此构造函数从编译器生成的代码所产生的类中调用。 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类定义了两个只读公有实例属性:Target和Method。给定一个委托对象的引用,我们可以查询这些属性。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操作符实例化委托
public delegate void CallBack(string name, int number); void PersonInfo(string name, int no) System.Console.WriteLine(name); System.Console.WriteLine(no); static void Main(string[] args) Program pr = new Program(); CallBack cb = new CallBack(pr.PersonInfo); cb("skynet", 23); }

 

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

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

 static void Main(string[] args)

 CallBack cb = new CallBack(Program.PersonInfo);

 cb("skynet", 23);

 }

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

3.2、用方法名实例化委托

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

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

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

using System;

using System.Collections.Generic;

using System.Text;

namespace DelegateTest

 class Program

 public delegate void CallBack(string name, int number);

 static void Main(string[] args)

 CallBack cb = delegate(string name, int number)

 System.Console.WriteLine(name);

 System.Console.WriteLine(number);

 cb("skynet",23);

}

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

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

 

3.4、Lambda表达式实例化委托

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

using System;

using System.Collections.Generic;

using System.Text;

namespace DelegateTest

 class Program

 public delegate void CallBack(string name, int number);

 static void Main(string[] args)

 CallBack cb = (name,number)= 

 System.Console.WriteLine(name);

 System.Console.WriteLine(number);

 cb("skynet",23);

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

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

4.1、协变委托

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

public delegate ICollection MyDelegate (int someParam, 

 string anotherParam); 

IList DoSomething(int someParam, string anotherParam)

 //Method implementation

MyDelegate del = DoSomething;

//As IList is child of ICollection thus, 

//Del is covariance delegate.
4.2、逆协变委托

 

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

public delegate ICollection MyDelegate (IList param);

ICollection DoSomething(ICollection param)

 //Method implementation

MyDelegate del = DoSomething;

//As IList is child of ICollection thus, 

//Del is contravariance delegate.

Offensive-Malware-Analysis-Dissecting-OSXFruitFly-Via-A-Custom-C&C-Server 立即下载