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

简介:

引言

委托(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中的委 托是类型安全的,委托会检测他所保存的函数引用是否和声明的委托匹配。下面的代码展示这个:

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委托时,编译器为我们做了如下工作,实际上定义任何委托编译器都会做如下工作:

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

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 = 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.

相关文章
|
17天前
|
开发框架 前端开发 .NET
C#编程与Web开发
【4月更文挑战第21天】本文探讨了C#在Web开发中的应用,包括使用ASP.NET框架、MVC模式、Web API和Entity Framework。C#作为.NET框架的主要语言,结合这些工具,能创建动态、高效的Web应用。实际案例涉及企业级应用、电子商务和社交媒体平台。尽管面临竞争和挑战,但C#在Web开发领域的前景将持续拓展。
|
17天前
|
SQL 开发框架 安全
C#编程与多线程处理
【4月更文挑战第21天】探索C#多线程处理,提升程序性能与响应性。了解C#中的Thread、Task类及Async/Await关键字,掌握线程同步与安全,实践并发计算、网络服务及UI优化。跟随未来发展趋势,利用C#打造高效应用。
|
17天前
|
存储 安全 网络安全
C#编程的安全性与加密技术
【4月更文挑战第21天】C#在.NET框架支持下,以其面向对象和高级特性成为安全软件开发的利器。本文探讨C#在安全加密领域的应用,包括使用System.Security.Cryptography库实现加密算法,利用SSL/TLS保障网络传输安全,进行身份验证,并强调编写安全代码的重要性。实际案例涵盖在线支付、企业应用和文件加密,展示了C#在应对安全挑战的同时,不断拓展其在该领域的潜力和未来前景。
|
17天前
|
人工智能 C# 开发者
C#编程中的图形界面设计
【4月更文挑战第21天】本文探讨了C#在GUI设计中的应用,介绍了Windows Forms、WPF和UWP等常用框架,强调了简洁界面、响应式设计和数据绑定等最佳实践。通过实际案例,展示了C#在企业应用、游戏开发和移动应用中的GUI实现。随着技术发展,C#在GUI设计的未来将趋向于跨平台、更丰富的组件和AI集成,为开发者创造更多可能性。
|
17天前
|
存储 算法 C#
C#编程与数据结构的结合
【4月更文挑战第21天】本文探讨了C#如何结合数据结构以构建高效软件,强调数据结构在C#中的重要性。C#作为面向对象的编程语言,提供内置数据结构如List、Array和Dictionary,同时也支持自定义数据结构。文章列举了C#实现数组、链表、栈、队列等基础数据结构的示例,并讨论了它们在排序、图算法和数据库访问等场景的应用。掌握C#数据结构有助于编写高性能、可维护的代码。
|
17天前
|
开发框架 Linux C#
C#编程的跨平台应用
【4月更文挑战第21天】C#与.NET Core的结合使得跨平台应用开发变得高效便捷,提供统一编程模型和高性能。丰富的类库、活跃的社区支持及Visual Studio Code、Xamarin等工具强化了其优势。广泛应用在企业系统、云服务和游戏开发中,虽面临挑战,但随着技术进步,C#在跨平台开发领域的前景广阔。
|
17天前
|
人工智能 C# 云计算
C#编程的未来发展趋向
【4月更文挑战第21天】C#编程未来将深化跨平台支持,强化云计算与容器技术集成,如.NET Core、Docker。在AI和ML领域,C#将提供更丰富框架,与AI芯片集成。语言和工具将持续创新,优化异步编程,如Task、async和await,提升多核性能。开源生态的壮大将吸引更多开发者,共创更多机遇。
|
17天前
|
程序员 C#
C#编程中的面向对象编程思想
【4月更文挑战第21天】本文探讨了C#中的面向对象编程,包括类、对象、封装、继承和多态。类是对象的抽象,定义属性和行为;对象是类的实例。封装隐藏内部细节,只暴露必要接口。继承允许类复用和扩展属性与行为,而多态使不同类的对象能通过相同接口调用方法。C#通过访问修饰符实现封装,使用虚方法和抽象方法实现多态。理解并应用这些概念,能提升代码的清晰度和可扩展性,助你成为更好的C#程序员。