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

简介:

引言

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

本文涉及主题如下:

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

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

1、委托初识

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

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

 
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4.  
  5. namespace DelegateTest  
  6. {  
  7.     class Program  
  8.     {  
  9.         public delegate void CallBack(string name, int number);  
  10.         void PersonInfo()  
  11.         { }  
  12.  
  13.         static void Main(string[] args)  
  14.         {  
  15.             Program pr = new Program();  
  16.             CallBack cb = pr.PersonInfo;  
  17.         }  
  18.     }  

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

C#Internals_Delegates1

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

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

 
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4.  
  5. namespace DelegateTest  
  6. {  
  7.     class Program  
  8.     {  
  9.         public delegate void CallBack(string name, int number);  
  10.         void PersonInfo(string name, int no)  
  11.         {  
  12.             System.Console.WriteLine(name);  
  13.             System.Console.WriteLine(no);  
  14.         }         
  15.  
  16.         static void Main(string[] args)  
  17.         {  
  18.             Program pr = new Program();  
  19.             CallBack cb = pr.PersonInfo;  
  20.             cb("skynet", 23);  
  21.         }  
  22.     }  

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

2、委托本质

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

C#Internals_Delegates1

图2、ILDasm查看DelegateTest.exe

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

  1. 1、声明一个类,对应上图中的.class nested public auto ansi sealed。
  2. 2、该类扩展自System.MutlicastDelegate,对应上图中的extends [mscorlib]System.MutlicastDelegate。
  3. 3、该类包含一个构造器,对应上图中的.ctor: void(object ,native int)。
  4. 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).gif1、MulticastDelegate()注意:仅.NET Compact Framework 2.0中支持,后面的版本3.5已经移除了它
  2. 2h7wdx6c.protmethod(zh-cn,VS.90).gif2、MulticastDelegate(Object target, String method):target——在其上定义 method 的对象,method——为其创建委托的方法的名称。此构造函数从编译器生成的代码所产生的类中调用。
  3. 2h7wdx6c.protmethod(zh-cn,VS.90).gif3、MulticastDelegate(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代码,如下图所示:

C#Internals_Delegates1

图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操作符实例化委托,如下代码所示:

 
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4.  
  5. namespace DelegateTest  
  6. {  
  7.     class Program  
  8.     {  
  9.         public delegate void CallBack(string name, int number);  
  10.         void PersonInfo(string name, int no)  
  11.         {  
  12.             System.Console.WriteLine(name);  
  13.             System.Console.WriteLine(no);  
  14.         }         
  15.  
  16.         static void Main(string[] args)  
  17.         {  
  18.             Program pr = new Program();              
  19.             CallBack cb = new CallBack(pr.PersonInfo);  
  20.             cb("skynet", 23);  
  21.         }  
  22.     }  

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

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

 
  1. static void Main(string[] args)  
  2.         {              
  3.             CallBack cb = new CallBack(Program.PersonInfo);  
  4.             cb("skynet", 23);  
  5.         } 

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

3.2、用方法名实例化委托

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

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

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

 
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4.  
  5. namespace DelegateTest  
  6. {  
  7.     class Program  
  8.     {  
  9.         public delegate void CallBack(string name, int number);  
  10.           
  11.         static void Main(string[] args)  
  12.         {  
  13.             CallBack cb = delegate(string name, int number)  
  14.             {  
  15.                 System.Console.WriteLine(name);  
  16.                 System.Console.WriteLine(number);  
  17.             };  
  18.             cb("skynet",23);  
  19.         }  
  20.     }  

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

C#Internals_Delegates1

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

3.4、Lambda表达式实例化委托

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

 
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4.  
  5. namespace DelegateTest  
  6. {  
  7.     class Program  
  8.     {  
  9.         public delegate void CallBack(string name, int number);  
  10.           
  11.         static void Main(string[] args)  
  12.         {  
  13.             CallBack cb = (name,number)=>  
  14.             {  
  15.                 System.Console.WriteLine(name);  
  16.                 System.Console.WriteLine(number);  
  17.             };  
  18.             cb("skynet",23);  
  19.         }  
  20.     }  

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

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

4.1、协变委托

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

 
  1. public delegate ICollection MyDelegate (int someParam,   
  2.                                  string anotherParam);   
  3.  
  4. IList DoSomething(int someParam, string anotherParam)  
  5. {  
  6.     //Method implementation  
  7. }  
  8.  
  9. MyDelegate del = DoSomething;  
  10.  
  11. //As IList is child of ICollection thus,   
  12. //Del is covariance delegate. 

4.2、逆协变委托

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

 
  1. public delegate ICollection MyDelegate (IList param);  
  2.  
  3. ICollection DoSomething(ICollection param)  
  4. {  
  5.     //Method implementation  
  6. }  
  7.  
  8. MyDelegate del = DoSomething;  
  9.  
  10. //As IList is child of ICollection thus,   
  11. //Del is contravariance delegate. 

 






     本文转自Saylor87 51CTO博客,原文链接:http://blog.51cto.com/skynet/365638,如需转载请自行联系原作者




相关文章
|
9天前
|
数据可视化 网络协议 C#
C#/.NET/.NET Core优秀项目和框架2024年3月简报
公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(每周至少会推荐两个优秀的项目和框架当然节假日除外),公众号推文中有项目和框架的介绍、功能特点、使用方式以及部分功能截图等(打不开或者打开GitHub很慢的同学可以优先查看公众号推文,文末一定会附带项目和框架源码地址)。注意:排名不分先后,都是十分优秀的开源项目和框架,每周定期更新分享(欢迎关注公众号:追逐时光者,第一时间获取每周精选分享资讯🔔)。
|
1月前
|
SQL 数据库 C#
C# .NET面试系列十一:数据库SQL查询(附建表语句)
#### 第1题 用一条 SQL 语句 查询出每门课都大于80 分的学生姓名 建表语句: ```sql create table tableA ( name varchar(10), kecheng varchar(10), fenshu int(11) ) DEFAULT CHARSET = 'utf8'; ``` 插入数据 ```sql insert into tableA values ('张三', '语文', 81); insert into tableA values ('张三', '数学', 75); insert into tableA values ('李四',
61 2
C# .NET面试系列十一:数据库SQL查询(附建表语句)
|
1月前
|
开发框架 算法 搜索推荐
C# .NET面试系列九:常见的算法
#### 1. 求质数 ```c# // 判断一个数是否为质数的方法 public static bool IsPrime(int number) { if (number < 2) { return false; } for (int i = 2; i <= Math.Sqrt(number); i++) { if (number % i == 0) { return false; } } return true; } class Progr
58 1
|
1月前
|
并行计算 安全 Java
C# .NET面试系列四:多线程
<h2>多线程 #### 1. 根据线程安全的相关知识,分析以下代码,当调用 test 方法时 i > 10 时是否会引起死锁? 并简要说明理由。 ```c# public void test(int i) { lock(this) { if (i > 10) { i--; test(i); } } } ``` 在给定的代码中,不会发生死锁。死锁通常是由于两个或多个线程互相等待对方释放锁而无法继续执行的情况。在这个代码中,只有一个线程持有锁,且没有其他线程参与,因此不
101 3
|
3月前
|
Linux C# 开发工具
C#开源的一款友好的.NET SDK管理器
C#开源的一款友好的.NET SDK管理器
|
2天前
|
开发框架 前端开发 JavaScript
采用C#.Net +JavaScript 开发的云LIS系统源码 二级医院应用案例有演示
技术架构:Asp.NET CORE 3.1 MVC + SQLserver + Redis等 开发语言:C# 6.0、JavaScript 前端框架:JQuery、EasyUI、Bootstrap 后端框架:MVC、SQLSugar等 数 据 库:SQLserver 2012
|
1月前
|
SQL 存储 关系型数据库
C# .NET面试系列十:数据库概念知识
#### 1. 为什么要一定要设置主键? 设置主键是数据库设计中的一个重要概念,有几个主要原因: 1、唯一性 ```c# 主键必须保证表中的每一行都有唯一的标识。这样可以避免数据冗余和不一致性。如果没有主键或者主键不唯一,就可能出现数据混乱或错误。 ``` 2、查询性能 ```c# 数据库系统通常会使用主键来加速数据检索。主键通常会被索引,这样可以更快速地找到特定行的数据,提高查询效率。 ``` 3、关联性 ```c# 主键常常用于建立表与表之间的关系。在关系数据库中,一个表的主键通常与其他表中的外键建立关联,这种关系对于数据的一致性和完整性非常重要。 ``` 4、数据完
130 1
C# .NET面试系列十:数据库概念知识
|
1月前
|
XML 开发框架 .NET
C# .NET面试系列八:ADO.NET、XML、HTTP、AJAX、WebService
## 第二部分:ADO.NET、XML、HTTP、AJAX、WebService #### 1. .NET 和 C# 有什么区别? .NET(通用语言运行时): ```c# 定义:.NET 是一个软件开发框架,提供了一个通用的运行时环境,用于在不同的编程语言中执行代码。 作用:它为多语言支持提供了一个统一的平台,允许不同的语言共享类库和其他资源。.NET 包括 Common Language Runtime (CLR)、基础类库(BCL)和其他工具。 ``` C#(C Sharp): ```c# 定义: C# 是一种由微软设计的面向对象的编程语言,专门为.NET 平台开发而创建。 作
173 2
|
1月前
|
开发框架 中间件 .NET
C# .NET面试系列七:ASP.NET Core
## 第一部分:ASP.NET Core #### 1. 如何在 controller 中注入 service? 在.NET中,在ASP.NET Core应用程序中的Controller中注入服务通常使用<u>依赖注入(Dependency Injection)</u>来实现。以下是一些步骤,说明如何在Controller中注入服务: 1、创建服务 首先,确保你已经在应用程序中注册了服务。这通常在Startup.cs文件的ConfigureServices方法中完成。例如: ```c# services.AddScoped<IMyService, MyService>(); //
60 0
|
1月前
|
开发框架 前端开发 .NET
C# .NET面试系列六:ASP.NET MVC
<h2>ASP.NET MVC #### 1. MVC 中的 TempData\ViewBag\ViewData 区别? 在ASP.NET MVC中,TempData、ViewBag 和 ViewData 都是用于在控制器和视图之间传递数据的机制,但它们有一些区别。 <b>TempData:</b> 1、生命周期 ```c# TempData 的生命周期是短暂的,数据只在当前请求和下一次请求之间有效。一旦数据被读取,它就会被标记为已读,下一次请求时就会被清除。 ``` 2、用途 ```c# 主要用于在两个动作之间传递数据,例如在一个动作中设置 TempData,然后在重定向到另
95 5