产品出库设备 发表于 2025-2-7 03:18:35

.NET Core 委托(Delegate)底层原理浅谈

简介

.NET通过委托来提供回调函数机制,与C/C++不同的是,委托确保回调是类型安全,且允许多播委托。并支持调用静态/实例方法。
简单来说,C++的函数指针有如下功能限制,委托作为C#中的上位替代,能弥补函数指针的不足。

[*]类型不安全
函数指针可以指向一个方法定义完全不同的函数。在编译期间不检查正确性。在运行时会导致签名不同导致程序崩溃
[*]只支持静态方法
只支持静态方法,不支持实例方法(只能通过邪道来绕过)
[*]不支持方法链
只能指向一个方法定义
函数指针与委托的相似之处

函数指针
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 00007ff9`521a19c1 48baa0040000c7010000 mov rdx,1C7000004A0h ("Hello from instance method")00007ff9`521a19cb 488b4908      mov   rcx,qword ptr 00007ff9`521a19cf 488b45f0      mov   rax,qword ptr 00007ff9`521a19d3 ff5018          call    qword ptr //重点00007ff9`521a19d6 90            nop               myDelegate.Invoke("Hello from static method");00007ff9`521a1a54 488b4de8      mov   rcx,qword ptr 00007ff9`521a1a58 48baf0040000c7010000 mov rdx,1C7000004F0h ("Hello from static method")00007ff9`521a1a62 488b4908      mov   rcx,qword ptr 00007ff9`521a1a66 488b45e8      mov   rax,qword ptr 00007ff9`521a1a6a ff5018          call    qword ptr //重点00007ff9`521a1a6d 90            nop可以看到,静态与实例都指向了rax+18h的地址偏移量。那么+18到底指向哪里呢?

Invoke的本质就是调用_methodPtr所在的函数指针
那么有人就会问了,前面源码里不是说了。静态方法的入口不是_methodPtrAux吗?怎么变成_methodPtr了。
实际上,如果是静态委托。JIT会生成一个桩方法,桩方法内部调用会+20偏移量的内容。从而调用_methodPtrAux
实例与静态核心代码的差异,大家有兴趣的话可以看一下它们的汇编

[*]实例方法核心代码
private void CtorClosed(object target, nint methodPtr){        if (target == null)        {                ThrowNullThisInDelegateToInstance();        }        _target = target;        _methodPtr = methodPtr;//_methodPtr真正承载了函数指针}
[*]静态方法核心代码
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);    }
[*]编译器层会拦截
这个很简单,在编译器中如果定义不匹配就会报错。

[*]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;                resultList = this;                if (followList == null)                {                  resultList = dFollow;                }                else                {                  for (int i = 0; i < followCount; i++)                        resultList = followList;                }                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并无区别

Lambda表达式

泛型委托是Lambd的基石,其底层还是委托那套东西
            var list = new List<string>();            var w= list.Where(x => x.Length > 100);
题外话:Lambda带来的闭包问题

闭包在JS中非常常见,究其原因是因为JS没有变量作用域。导致读取其他函数内部变量的函数,因此在特定情况下会引发bug,特别是在异步与循环中
      public static void Method1()      {            var age = 35;            Action action = new Action(() =>            {                Console.WriteLine($"你今年多少岁了?{age}岁");            });            age = 40;            action();      }      public static void Method2()      {            List<Action> list=new List<Action>();            for ( int i = 0; i < 3; i++ )            {                list.Add(new Action(() => { Console.WriteLine($"当前i为{i}"); }));            }            foreach ( Action action in list )            {                action();            }      }输出结果为:

究其原因,就是当闭包方法a执行时,如果它引用着其它函数b的变量。那么方法a会保留方法b最后执行的变量内容。所以输出结果与正常非闭包方法不一致。
眼见为实

上IL代码

IL代码中,先将35传递给委托方法,再40传递给委托方法,最后再执行invoke.
可以简单抽象为如下代码:
            var dc = new DisplayClass2_0() { age = 35 };            Action action = dc.UpdateCase_b__0;            dc.age = 40;            action();如何解决闭包?


[*]用魔法对抗魔法
      static void UpdateCaseNew()      {            //1. 用魔法对抗魔法            int age = 35;            int classage = age;//魔法语句 (让它作为类变量)            Action action = () => Console.WriteLine($"你今年多少岁了?{classage}岁");            age = 40;            action();      }
[*]避免闭包,使用参数传递.
      static void UpdateCaseNew2()      {            int age = 35;            Action<int> action = new Action<int>(x =>            {                Console.WriteLine("{0}岁了,大龄啦!", x);            });            age = 40;            action(35);      }事件与委托的关系

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

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

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

实际上在底层,还是操作Delegate.Combine那一套东西
页: [1]
查看完整版本: .NET Core 委托(Delegate)底层原理浅谈