简介

.NET通过委托来提供回调函数机制,与C/C++不同的是,委托确保回调是类型安全,且允许多播委托。并支持调用静态/实例方法。

简单来说,C++的函数指针有如下功能限制,委托作为C#中的上位替代,能弥补函数指针的不足。

  1. 类型不安全

    函数指针可以指向一个方法定义完全不同的函数。在编译期间不检查正确性。在运行时会导致签名不同导致程序崩溃
  2. 只支持静态方法

    只支持静态方法,不支持实例方法(只能通过邪道来绕过)
  3. 不支持方法链

    只能指向一个方法定义

函数指针与委托的相似之处

函数指针

typedef int (*func)(int, int);

委托

delegate int func(int a, int b);

委托底层模型

delegate关键字作为语法糖,IL层会为该关键字自动生成Invoke/BeginInvoke/EndInvoke方法,在.NET Core中,不再支持BeginInvoke/EndInvoke

眼见为实

    public abstract partial class Delegate : ICloneable, ISerializable
{
// _target is the object we will invoke on
internal object? _target; // 源码中的注释不太对(null if static delegate)。应该是这样:如果注册的是实例方法,则是this指针,如果是静态则是delegate实例自己。 // MethodBase, either cached after first request or assigned from a DynamicMethod
// For open delegates to collectible types, this may be a LoaderAllocator object
internal object? _methodBase; //缓存 // _methodPtr is a pointer to the method we will invoke
// It could be a small thunk if this is a static or UM call
internal IntPtr _methodPtr;//实例方法的入口,看到IntPtr关键字就知道要与非托管堆交互,必然就是函数指针了, // In the case of a static method passed to a delegate, this field stores
// whatever _methodPtr would have stored: and _methodPtr points to a
// small thunk which removes the "this" pointer before going on
// to _methodPtrAux.
internal IntPtr _methodPtrAux;//静态方法的入口
}
    public abstract class MulticastDelegate : Delegate
{
//多播委托的底层基石
private object? _invocationList;
private nint _invocationCount; //实例委托调用此方法
private void CtorClosed(object target, IntPtr methodPtr)
{
if (target == null)
ThrowNullThisInDelegateToInstance();
this._target = target;
this._methodPtr = methodPtr;//函数指针被指向_methodPtrAux
}
//静态委托调用此方法
private void CtorOpened(object target, IntPtr methodPtr, IntPtr shuffleThunk)
{
this._target = this;//上面说到,_target的注释不对的判断就在此
this._methodPtr = shuffleThunk;//与实例委托不同,这里被指向一个桩函数
this._methodPtrAux = methodPtr;//函数指针被指向_methodPtrAux
}
}

委托如何同时支持静态方法与实例方法?

示例代码
        static void Main(string[] args)
{
//1.注册实例方法
MyClass myObject = new MyClass();
MyDelegate myDelegate2 = new MyDelegate(myObject.InstanceMethod);
myDelegate2.Invoke("Hello from instance method"); Debugger.Break(); //2.注册静态方法
MyDelegate myDelegate = MyClass.StaticMethod;
myDelegate.Invoke("Hello from static method"); Debugger.Break(); }
} public delegate void MyDelegate(string message); public class MyClass
{
public static void StaticMethod(string message)
{
Console.WriteLine("Static Method: " + message);
} public void InstanceMethod(string message)
{
Console.WriteLine("Instance Method: " + message);
}
}
            myDelegate2.Invoke("Hello from instance method");
00007ff9`521a19bd 488b4df0 mov rcx,qword ptr [rbp-10h]
00007ff9`521a19c1 48baa0040000c7010000 mov rdx,1C7000004A0h ("Hello from instance method")
00007ff9`521a19cb 488b4908 mov rcx,qword ptr [rcx+8]
00007ff9`521a19cf 488b45f0 mov rax,qword ptr [rbp-10h]
00007ff9`521a19d3 ff5018 call qword ptr [rax+18h] //重点
00007ff9`521a19d6 90 nop
            myDelegate.Invoke("Hello from static method");
00007ff9`521a1a54 488b4de8 mov rcx,qword ptr [rbp-18h]
00007ff9`521a1a58 48baf0040000c7010000 mov rdx,1C7000004F0h ("Hello from static method")
00007ff9`521a1a62 488b4908 mov rcx,qword ptr [rcx+8]
00007ff9`521a1a66 488b45e8 mov rax,qword ptr [rbp-18h]
00007ff9`521a1a6a ff5018 call qword ptr [rax+18h] //重点
00007ff9`521a1a6d 90 nop

可以看到,静态与实例都指向了rax+18h的地址偏移量。那么+18到底指向哪里呢?

Invoke的本质就是调用_methodPtr所在的函数指针

那么有人就会问了,前面源码里不是说了。静态方法的入口不是_methodPtrAux吗?怎么变成_methodPtr了。

实际上,如果是静态委托。JIT会生成一个桩方法,桩方法内部调用会+20偏移量的内容。从而调用_methodPtrAux

实例与静态核心代码的差异,大家有兴趣的话可以看一下它们的汇编

  1. 实例方法核心代码
private void CtorClosed(object target, nint methodPtr)
{
if (target == null)
{
ThrowNullThisInDelegateToInstance();
}
_target = target;
_methodPtr = methodPtr;//_methodPtr真正承载了函数指针
}
  1. 静态方法核心代码
private void CtorOpened(object target, nint methodPtr, nint shuffleThunk)
{
_target = this;
_methodPtr = shuffleThunk;//_methodPtr只是一个桩函数
_methodPtrAux = methodPtr;//真正的指针在_methodPtrAux中
}

委托如何支持类型安全?

点击查看代码
    internal class Program
{
static void Main(string[] args)
{
//1. 编译器层面错误
//var myDelegate = new MyDelegate(Math.Max); //2. 运行时层类型转换错误
var myDelegate = new MyDelegate(Console.WriteLine);
MyMaxDelegate myMaxDelegate = (MyMaxDelegate)(object)myDelegate; Debugger.Break();
} public delegate void MyDelegate(string message);
public delegate int MyMaxDelegate(int a, int b); }
  1. 编译器层会拦截

    这个很简单,在编译器中如果定义不匹配就会报错。

  2. CLR Runtime会在汇编中插入检查命令

    检查不一致会报错,不至于整个程序奔溃。

委托如何支持多播?

多播委托的添加

委托使用+=或者Delegate.Combine来添加新的委托。其底层调用的是CombineImpl,由子类MulticastDelegate实现。

并最终产生一个新的委托

for循环1000次Combine委托,会产生1000个对象,

		//简化版
protected sealed override Delegate CombineImpl(Delegate? follow)
{
MulticastDelegate dFollow = (MulticastDelegate)follow;
object[]? resultList;
int followCount = 1;
object[]? followList = dFollow._invocationList as object[];
if (followList != null)
followCount = (int)dFollow._invocationCfollowListount; int resultCount;
if (!(_invocationList is object[] invocationList))
{
resultCount = 1 + followCount;
resultList = new object[resultCount];
resultList[0] = this;
if (followList == null)
{
resultList[1] = dFollow;
}
else
{
for (int i = 0; i < followCount; i++)
resultList[1 + i] = followList[i];
}
return NewMulticastDelegate(resultList, resultCount);
}
//xxxxxxxxxx
}
//关键核心,将组合后的Delegate组成一个新对象,并填充invocationList,invocationCount
private MulticastDelegate NewMulticastDelegate(object[] invocationList, int invocationCount, bool thisIsMultiCastAlready)
{
// First, allocate a new multicast delegate just like this one, i.e. same type as the this object
MulticastDelegate result = (MulticastDelegate)InternalAllocLike(this); // Performance optimization - if this already points to a true multicast delegate,
// copy _methodPtr and _methodPtrAux fields rather than calling into the EE to get them
if (thisIsMultiCastAlready)
{
result._methodPtr = this._methodPtr;
result._methodPtrAux = this._methodPtrAux;
}
else
{
result._methodPtr = GetMulticastInvoke();
result._methodPtrAux = GetInvokeMethod();
}
result._target = result;
result._invocationList = invocationList;
result._invocationCount = invocationCount; return result;
}

多播委托的执行

上面提到,Invoke的本质就是调用_methodPtr所在的函数指针.

那么自然而然,负责执行多播肯定就是_methodPtr了。

从上面的源码可以知道,MulticastDelegate在初始化的时候要调用一次GetMulticastInvoke(),让我们来看看它是什么?

哦豁,它还是一个非托管的方法,有兴趣的同学可以自行查看coreclr的c++源码。奥秘就在其中,本人水平有限,怕误人子弟。

简单来说,就是_methodPtr方法在coreclr底层,for循环执行invocationList的委托队列。

思考一个问题,如果只是一个简单的for循环,其中一个委托卡死/执行失败,怎么办?

提示:MulticastDelegate类中有很多override method

非托管委托(函数指针)

C#作为C++的超集,也别名为C++++ 。也可以说是C++的手动挡(JAVA是C++的自动挡)。

自然而然,C++有的,C#也要有。因此在C#11中引入了函数指针,性能更强的同时也继承了C++的所有缺点(除了会在编译期间协助类型安全检查).

https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/unsafe-code#function-pointers

泛型委托

为了减轻你工作量,避免创建太多委托定义。BCL提供了Action/Func来提供便利,减少你的代码量。

它们在底层与delegate并无区别

事件与委托的关系

CLR事件模型以委托为基础,它们之间的关系。像是对委托的进一步封装。

  1. 事件就是一个语法糖,自己自身并没有新概念
  2. 委托和事件的关系”等同于“字段和属性的关系”



事件作为语法糖,IL会在底层生成一个委托并提供add_xxxx与remove_xxxx方法对委托进行封装。



实际上在底层,还是操作Delegate.Combine那一套东西

.NET Core 委托底层原理浅谈的更多相关文章

  1. Java线上问题排查神器Arthas快速上手与原理浅谈

    前言 当你兴冲冲地开始运行自己的Java项目时,你是否遇到过如下问题: 程序在稳定运行了,可是实现的功能点了没反应. 为了修复Bug而上线的新版本,上线后发现Bug依然在,却想不通哪里有问题? 想到可 ...

  2. CSRF漏洞原理浅谈

    CSRF漏洞原理浅谈 By : Mirror王宇阳 E-mail : mirrorwangyuyang@gmail.com 笔者并未深挖过CSRF,内容居多是参考<Web安全深度剖析>.& ...

  3. 如何把Java代码玩出花?JVM Sandbox入门教程与原理浅谈

    在日常业务代码开发中,我们经常接触到AOP,比如熟知的Spring AOP.我们用它来做业务切面,比如登录校验,日志记录,性能监控,全局过滤器等.但Spring AOP有一个局限性,并不是所有的类都托 ...

  4. JAVA CAS原理浅谈

    java.util.concurrent包完全建立在CAS之上的,没有CAS就不会有此包.可见CAS的重要性. CAS CAS:Compare and Swap, 翻译成比较并交换. java.uti ...

  5. CAS+SSO原理浅谈

    http://www.cnblogs.com/yonsin/archive/2009/08/29/1556423.htmlSSO 是一个非常大的主题,我对这个主题有着深深的感受,自从广州 UserGr ...

  6. php模板原理PHP模板引擎smarty模板原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  7. PHP的模板引擎smarty原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  8. Docker 基础底层架构浅谈

    docker学习过程中,免不了需要学习下docker的底层技术,今天我们来记录下docker的底层架构吧! 从上图我们可以看到,docker依赖于linux内核的三个基本技术:namespaces.C ...

  9. Java中的SPI原理浅谈

    在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则& ...

  10. JDK source 之 LinkedHashMap原理浅谈

    注:本文参考JDK1.7.0_45源码. LinkedHashMap是基于HashMap实现的数据结构,与HashMap主要的不同为每个Entry是使用双向链表实现的,并且提供了根据访问顺序进行排序的 ...

随机推荐

  1. Antd-React-TreeSelect前端搜索过滤

    在开发过程中,但是antd中的搜索会把多余的也会带出来 就例如下图,我们本想去搜索1但是他会把其子节点都带出来,其实我们的本意是像搜2一样或者当中间隔层处理 但是我们该如何解决这样的问题呢如何做到下面 ...

  2. Linux-centos中修改默认root帐户的登录用户名

    vi /etc/passwd 按i键进入编辑状态 修改第1行第1个root为新的用户名 按esc键退出编辑状态,并输入:x保存并退出 vi /etc/shadow 按i键进入编辑状态 修改第1行第1个 ...

  3. Flask细说

    Flask框架 简介 特点: 微框架,间接,给开发者提供很大的扩展性 Flask和相应的插件写得很好,用起来很爽. 开发效率非常高,比如使用 SQLAlchemy 的 ORM 操作数据库可以节省开发者 ...

  4. zblog免费插件分享前端代码支持一键复制

    zblog默认的代码文件在网页前端是不支持一键复制的,这会让访客复制长代码的时候不太方便,甚至有可能会出错,影响体验,下面分享一个非常简单的免费插件,安装之后,前端代码就能一键复制了. 插件使用方法: ...

  5. python requests 报错 Caused by ProxyError ('Unable to connect to proxy', OSError('Tunnel connection failed: 403 Tunnel or SSL Forbidden'))

    背景:访问https接口,使用http代理 版本:requests: 2.31.0 从报错可以看出,是proxy相关的报错 调整代码,设定不使用代理,将http与https对应的proxy值置空即可( ...

  6. IDEA 忽然无法打开某个特定文件

    背景:IDEA中双击打开一个.py文件时,弹出一个文件类型的弹窗(没注意是什么,估计是不小心按到了什么快捷键),当时随便选的Text,结果不知道为什么,这个文件无法在IDEA中打开(之前都正常) 由于 ...

  7. java-GUI编程之布局类型介绍

    java使用AWT和Swing相关的类可以完成图形化界面编程,其中AWT的全称是抽象窗口工具集(Abstract Window Toolkit),它是sun公司最早提供的GUI库,这个GUI库提供了一 ...

  8. Unity 中 Color 与 Color32 的区别

    1. 存储方式 Color用四个浮点数(float)来表示RGBA,取值范围均是0到1 举例: var orange = new Color(1f, 0.5f, 0f, 1f); 而Color32使用 ...

  9. Maven高级——分模块开发与设计

    分模块开发的意义 将原始模块按照功能拆分成若干个子模块,方便模块间的相互调用,接口共享 分模块开发 创建Maven工程 书写模块代码 注意:分模块开发需要先针对模块功能进行设计,再进行编码.不会先将工 ...

  10. 进程切换分析(2):TLB处理

    一.前言 进程切换是一个复杂的过程,本文不准备详细描述整个进程切换的方方面面,而是关注进程切换中一个小小的知识点:TLB的处理.为了能够讲清楚这个问题,我们在第二章描述在单CPU场景下一些和TLB相关 ...