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

简介:

2010-04-11 12:32 by 吴秦, 1764 阅读, 0 评论, 收藏, 编辑

引言

上篇.NET (C#) Internals: Delegates (1)我们介绍了委托初识、委托本质、委托的实例化方式、协变委托与逆协变委托,本篇将介绍如下主题:

  • 1、委托链直观
  • 2、委托链的本质
    • 2.1、+=操作
    • 2.2、-=操作
  • 3、委托链结构
  • 4、委托链的返回值问题

1、委托链直观

在上篇中我们知道调用委托,我们只需要简单地以函数调用的方法,如上篇的例子中cb(“skynet”,23)调用委托cb,实际上调用的是PersonInfo方法。因为我们将PersonInfo方法与委托cb绑定了,且委托只绑定了一个方法。大家应该见过类似于下面的代码(注意其中的+=、-=):

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);
        }
        void PersonInfo1(string name, int no)
        {
            System.Console.WriteLine("Person's name is: "+name);
            System.Console.WriteLine("Person's number is: "+no);
        }

        static void Main(string[] args)
        {
            Program pr = new Program();
            CallBack cb = pr.PersonInfo;
            cb += pr.PersonInfo1;
            cb("skynet", 23);
            System.Console.WriteLine("-----------------------");
            cb -= pr.PersonInfo;
            cb("skynet", 23);
        }
    }
}

运行上述代码会有如下结果:

image 图1、委托链实例

从结果来看,我们知道第一次调用cb(“skynet”,23)时,调用了两个方法PersonInfo、PersonInfo1;第二次调用cb(“skynet”,23)时,只调用了PersonInfo1。从源码我们可以看出是什么造成了这样的输出结果:

第一次调用cb(“skynet”,23)跟我们上篇中第一节委托初识中的代码相比只是多了cb += pr.PersonInfo1及相应的PersonInfo1方法,这是造成委托调用了两个方法的原因所在,+=操作将PersonInfo1方法追加到委托使其绑定两个方法,而且调用时是按照绑定的顺序调用方法的(试试先绑定PersonInfo1,再+=PersonInfo,然后调用cb(“skynet”,23)就知道了!)。

第二次调用cb(“skynet”,23)是在cb -= pr.PersonInfo,结果委托知道了PersonInfo1方法,说明-=操作移除了PersonInfo方法与委托cb的绑定。

总之,我们可以知道+=、-=操作分别可以向委托追加绑定方法、移除指定方法与委托的绑定。

2、委托链本质

我们在用+=、-=操作时,编译器为我们做了什么呢?让我们用ILDasm查看刚才生成的DelegateTest可执行文件,如下图: image

图2、ILDasm查看DelegateTest可执行文件

其中的Main方法的IL代码如下所示:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       108 (0x6c)
  .maxstack  4
  .locals init ([0] class DelegateTest.Program pr,
           [1] class DelegateTest.Program/CallBack cb)
  IL_0000:  nop
  IL_0001:  newobj     instance void DelegateTest.Program::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldftn      instance void DelegateTest.Program::PersonInfo(string,
                                                                      int32)
  IL_000e:  newobj     instance void DelegateTest.Program/CallBack::.ctor(object,
                                                                          native int)
  IL_0013:  stloc.1
  IL_0014:  ldloc.1
  IL_0015:  ldloc.0
  IL_0016:  ldftn      instance void DelegateTest.Program::PersonInfo1(string,
                                                                       int32)
  IL_001c:  newobj     instance void DelegateTest.Program/CallBack::.ctor(object,
                                                                          native int)
  IL_0021:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
                                                                                          class [mscorlib]System.Delegate)
  IL_0026:  castclass  DelegateTest.Program/CallBack
  IL_002b:  stloc.1
  IL_002c:  ldloc.1
  IL_002d:  ldstr      "skynet"
  IL_0032:  ldc.i4.s   23
  IL_0034:  callvirt   instance void DelegateTest.Program/CallBack::Invoke(string,
                                                                           int32)
  IL_0039:  nop
  IL_003a:  ldstr      "-----------------------"
  IL_003f:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0044:  nop
  IL_0045:  ldloc.1
  IL_0046:  ldloc.0
  IL_0047:  ldftn      instance void DelegateTest.Program::PersonInfo(string,
                                                                      int32)
  IL_004d:  newobj     instance void DelegateTest.Program/CallBack::.ctor(object,
                                                                          native int)
  IL_0052:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Remove(class [mscorlib]System.Delegate,
                                                                                         class [mscorlib]System.Delegate)
  IL_0057:  castclass  DelegateTest.Program/CallBack
  IL_005c:  stloc.1
  IL_005d:  ldloc.1
  IL_005e:  ldstr      "skynet"
  IL_0063:  ldc.i4.s   23
  IL_0065:  callvirt   instance void DelegateTest.Program/CallBack::Invoke(string,
                                                                           int32)
  IL_006a:  nop
  IL_006b:  ret
} // end of method Program::Main


2.1、+=操作

Main方法的IL代码IL_0021: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) 对应的就是+=操作的IL代码。+=实际上是调用了System.Delegate类的Combine(Delegate,Delegate)方法,参数为两个委托对象。之所以能调用System.Delegate类的方法,是因为System.MulticastDelegate类继承自它,而我们所有的委托都继承自System.MulticastDelegate类。事实上,如果你查MSDN你会发现System.Delegate类的Combine是重载的,共有两个重载,如下所示:

  1. Delegate.Combine (Delegate[])
    将委托数组的调用列表连接在一起。

    如果 delegates 数组包含为空引用(在 Visual Basic 中为 Nothing) 的项,则将忽略这些项。调用列表中可包含重复项目,即引用同一对象上同一种方法的项目。

  2. Delegate.Combine (Delegate, Delegate)
    将两个委托的调用列表连接在一起。由 .NET Compact Framework 支持。 调用列表中可包含重复项目,即引用同一对象上同一种方法的项目。注意:第一个参数的委托会被先调,第二个参数的委托会被后调用。

2.2、-=操作

Main方法的IL代码中,-=对应IL_0052: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Remove(class [mscorlib]System.Delegate,class [mscorlib]System.Delegate)。即-=操作实际上是调用了System.Delegate类的Remove(Delegate source,Delegate value)方法。source——将从中移除 value 的调用列表,value——它提供将从其中移除 source 的调用列表的调用列表。该方法的返回值是一个新委托,其调用列表的构成方法为:获取 source 的调用列表,如果在 source 的调用列表中找到了 value 的调用列表,则从中移除 value 的最后一个调用列表。如果 value 为 空引用(在 Visual Basic 中为 Nothing),或在 source 的调用列表中没有找到 value 的调用列表,则返回 source。如果 value 的调用列表等于 source 的调用列表,或 source 为空引用,则返回空引用。注意:如果 value 的调用列表在 source 的调用列表中多次出现,则移除最后一个匹配项。就是移除后绑定的调用列表,运行下面代码就可以得出比较清晰的理解:

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);
        }
        void PersonInfo1(string name, int no)
        {
            System.Console.WriteLine("Person1's name is: "+name);
            System.Console.WriteLine("Person1's number is: "+no);
        }
        void PersonInfo2(string name, int no)
        {
            System.Console.WriteLine("Person2's name is: " + name);
            System.Console.WriteLine("Person2's number is: " + no);
        }
        static void Main(string[] args)
        {
            Program pr = new Program();
            CallBack cb = pr.PersonInfo;
            cb += pr.PersonInfo1;
            cb += pr.PersonInfo2;
            cb += pr.PersonInfo;
            cb("skynet", 23);
            System.Console.WriteLine("-----------------------");
            cb -= pr.PersonInfo;
            cb("skynet", 23);
        }
    }
}


如果你想移除匹配的所有调用列表的话,请使用RemoveAll方法。

public static Delegate RemoveAll ( Delegate source, Delegate value ):如果 value 的调用列表与 source 的调用列表中一组相邻的元素相匹配,则认为 source 的调用列表内存在 value 的调用列表。如果 value 的调用列表在 source 的调用列表中多次出现,则移除所有调用列表。

3、委托链结构

上篇中的委托本质中知道,任何委托都继承自[mscorlib]System.MutlicastDelegate,且其中有一个类型为System.MutlicastDelegate的私有字段_prev,它指向另一个委托对象,通常为空。没错就是这个字段使我们能将多个委托组成一个链成为了可能,此时除了链头_prev都不为空!_prev字段指向前一个委托对象,所以上面例子中的PersonInfo和PersonInfo1组成的委托链可表示为:

绘图1图3、委托链

如果再+=绑定一个PersonInfo2方法,将在PersonInfo2的_prev字段指向PersonInfo1,链上的其他内容不变。那我们再来分析一下当绑定多个方法时,Invoke方法到底是怎么执行的呢?它内部代码大概逻辑如下:

public void Invoke(string,int)
{
  if (_prve != null) _prve.Invoke(string ,int);  //如果当前节点的_prev字段不空,就递归调用_prev指向的节点的Invoke方法,直至调用最先绑定的方法。

  _target.method(string ,int);  //最后调用最后绑定的方法
}

其实完全可以把我们在数据结构中学的链表知识运用在此来理解委托,或者说委托链就是一个链表。

4、委托链的返回值问题

上面的委托链例子是委托的返回类型为void的情况,想象一下如果委托的返回类型不空,它是会返回每个委托调用的返回值,还是?让我们看看下面这段代码:

using System;
using System.Collections.Generic;
using System.Text;

namespace DelegateTest
{
    class Program
    {
        public delegate string CallBack(string name, int number);
       
        string PersonInfo1(string name, int no)
        {
            System.Console.WriteLine("调用了PersonInfo1");
            return "Person1's name is " + name + ",number is " + no;
        }
        string PersonInfo2(string name, int no)
        {
            System.Console.WriteLine("调用了PersonInfo2");
            return "Person2's name is " + name + ",number is " + no;
        }
        static void Main(string[] args)
        {
            Program pr = new Program();            
            CallBack cb = pr.PersonInfo1;
            cb += pr.PersonInfo2;            
            
            System.Console.WriteLine("最终返回结果: "+cb("skynet",23));
        }
    }
}

我们定义的委托的返回类型为string类型,委托实例cb与PersonInfo1和PersonInfo2绑定,运行结果如下图所示:

image图4、委托链的返回值

从结果来看我们知道:调用cb("skynet",23)时,PersonInfo1和PersonInfo2都被调用了,但只返回了PersonInfo2的返回值。 可见调用委托链时,只返回最后一个的委托调用!如果我们想返回每个委托调用的返回值,我们该怎么做呢?

设想一下如下情况:我们调用cb("skynet",23)时,它只会返回PersonInfo2的返回值,那我们可不可以获取委托链中绑定的每个方法,然后我们设计自己的调用规则,每次调用绑定方法时将其返回值保持在一个数组里再返回呢?

System.MutlicastDelegate中有一个public sealed override Delegate[] GetInvocationList()方法,它就是返回委托链的调用列表的,它的代码如下:

public sealed override Delegate[] GetInvocationList()
{
    object[] objArray = this._invocationList as object[];
    if (objArray == null)
    {
        return new Delegate[] { this };
    }
    int num = (int) this._invocationCount;
    Delegate[] delegateArray = new Delegate[num];
    for (int i = 0; i < num; i++)
    {
        delegateArray[i] = (Delegate) objArray[i];
    }
    return delegateArray;
}

安装上面的思路我们写出如下代码:

using System;
using System.Collections.Generic;
using System.Text;

namespace DelegateTest
{
    class Program
    {
        public delegate string CallBack(string name, int number);
       
        string PersonInfo1(string name, int no)
        {
            System.Console.WriteLine("调用了PersonInfo1");
            return "Person1's name is " + name + ",number is " + no;
        }
        string PersonInfo2(string name, int no)
        {
            System.Console.WriteLine("调用了PersonInfo2");
            return "Person2's name is " + name + ",number is " + no;
        }
        static void Main(string[] args)
        {
            Program pr = new Program();            
            CallBack cb = pr.PersonInfo1;
            cb += pr.PersonInfo2; 
            
            StringBuilder strs = new StringBuilder();
            Delegate[] arrayDelegates = cb.GetInvocationList();
            foreach (CallBack d in arrayDelegates)
            {
                strs.AppendFormat("{0}{1}",d("skynet",23),Environment.NewLine);
            }
            System.Console.WriteLine("最终返回结果:");
            System.Console.WriteLine(strs.ToString());
        }
    }
}

运行结果如下图所示:

image图5、返回每个委托调用的返回值

从运行结果可以看出,我们思路是可行的,的确达到了返回每个委托调用的返回值的目的。




本文转自吴秦博客园博客,原文链接:http://www.cnblogs.com/skynet/archive/2010/04/11/1709455.html,如需转载请自行联系原作者

相关文章
|
17天前
|
数据可视化 网络协议 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 ('李四',
66 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); } } } ``` 在给定的代码中,不会发生死锁。死锁通常是由于两个或多个线程互相等待对方释放锁而无法继续执行的情况。在这个代码中,只有一个线程持有锁,且没有其他线程参与,因此不
105 3
|
3月前
|
Linux C# 开发工具
C#开源的一款友好的.NET SDK管理器
C#开源的一款友好的.NET SDK管理器
|
11天前
|
开发框架 前端开发 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月前
|
开发框架 人工智能 .NET
C#/.NET/.NET Core拾遗补漏合集(持续更新)
C#/.NET/.NET Core拾遗补漏合集(持续更新)
|
1月前
|
SQL 存储 关系型数据库
C# .NET面试系列十:数据库概念知识
#### 1. 为什么要一定要设置主键? 设置主键是数据库设计中的一个重要概念,有几个主要原因: 1、唯一性 ```c# 主键必须保证表中的每一行都有唯一的标识。这样可以避免数据冗余和不一致性。如果没有主键或者主键不唯一,就可能出现数据混乱或错误。 ``` 2、查询性能 ```c# 数据库系统通常会使用主键来加速数据检索。主键通常会被索引,这样可以更快速地找到特定行的数据,提高查询效率。 ``` 3、关联性 ```c# 主键常常用于建立表与表之间的关系。在关系数据库中,一个表的主键通常与其他表中的外键建立关联,这种关系对于数据的一致性和完整性非常重要。 ``` 4、数据完
131 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 平台开发而创建。 作
174 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>(); //
65 0