为什么在 IL 代码中找不到委托的 Invoke 方法体?

Why can't I find the Invoke method body of a delegate in IL code?

我为 TestDelegate

反编译了源代码
public delegate int TestDelegate(int a, int b);

为什么我在查看这段IL代码时找不到Invoke方法?我在委托中也找不到其他方法。它是如何工作的?

  .method public hidebysig virtual newslot instance int32
    Invoke(
      int32 a,
      int32 b
    ) runtime managed
  {
    // Can't find a body
  } // end of method TestDelegate::Invoke
     TestDelegate SumDelegate = Sum;
     SumDelegate.Invoke(1, 2);

IL:

    IL_001c: callvirt     instance int32 Resolvers.Tests.Delegates.TestDelegate::Invoke(int32, int32)

正在生成 IL 显示 Invoke 方法调用,我找不到它。究竟是怎么回事?

因为委托是对方法的引用,而不是实际方法。

它没有在您的 C# 代码上实现,那么是什么让您认为它可以在生成的 IL 代码中有任何类型的实现?

来自Delegates (C# Programming Guide)

A delegate is a type that represents references to methods with a particular parameter list and return type. When you instantiate a delegate, you can associate its instance with any method with a compatible signature and return type. You can invoke (or call) the method through the delegate instance.

委托上的 Invoke(...) 方法(以及其他一些方法,如 BeginInvoke(...)EndInvoke(...))由运行时本身实现,而不是在程序集中实现,它这就是为什么在反编译时看不到方法体的原因。这些方法附加了一个属性来指示这一点,例如:

[MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)]
public virtual int Invoke(int a, int b);

问它是如何工作的 "under the hood" 当然是合理的,虽然答案很复杂,因为它取决于你的方法的 种类 委托是调用(例如静态方法与实例方法、虚拟方法与非虚拟方法等)以及委托是 "open" 还是 "closed".

虽然 "open" 和 "closed" 不是我们通常在委托上下文中遇到的术语,但其含义相对简单 - "closed" 委托存储方法的第一个参数在静态方法的情况下将被调用,或者在实例方法的情况下将调用该方法的实例(即 this),而 "open" 委托则不会。如果您有兴趣,This post 包含更多详细信息。为简单起见,我将仅介绍您最有可能遇到的两种类型 - 实例封闭委托和静态开放委托。

您可能还注意到在您的反编译中您的 TestDelegate 派生自 System.Delegate(通过 System.MulticastDelegate),因此继承了 4 个字段,您可以在 . NET Core 运行时源代码 here。以下三个与我们最相关:

object _target;
IntPtr _methodPtr;
IntPtr _methodPtrAux;

值得注意的是,在委托上调用 Invoke(...) 总是做同样的事情 - 它加载委托的 _target 作为第一个参数(例如方法,第一个参数是我们通常所说的 this), 然后调用 _methodPtr 指向的方法,这使得对实例方法的委托非常简单,因为它几乎与直接调用实例方法完全一样,但对于静态方法来说稍微复杂一些,因为我们将见下文。

首先从最简单的情况开始,并以您的 TestDelegate 为例,您将创建一个实例关闭委托,如下所示:

public class Test
{
    private int _c;

    ...

    public int Add(int a, int b)
    {
        return a + b + _c;
    }
}

...

var testInstance = new Test();
var addDelegate = new TestDelegate(testInstance.Add);

addDelegate 是实例关闭委托,因为它存储将调用 Add(...) 方法的实例 (testInstance)。在这种情况下,_target字段将存储testInstance,而_methodPtr存储Test.Add(...)方法的地址。

当您随后调用 addDelegate.Invoke(...)(或等效的短格式 addDelegate(...))时,testInstance 会从 _target 字段加载到 thisAdd(...) 方法的地址从 _methodPtr 字段加载并被调用,因此几乎与直接调用 testInstance.Add(...) 完全一样。

对于静态开放委托,您可以这样做:

public class Test
{
    public static int Add(int a, int b)
    {
        return a + b;
    }
}

var addDelegate = new TestDelegate(Test.Add);

这里,addDelegate是一个静态的open delegate,是一个稍微复杂一点的场景。在这种情况下没有实例,因为 Test.Add(...) 是静态的,但由于 Invoke(...) 总是以相同的方式工作,如果要在 _methodPtr 中存储指向 Test.Add(...) 的指针,我们会有问题,因为参数会在错误的位置 - _target 的内容会在第一个参数位置,ab 会在第二和第三个参数位置, 当他们需要在第 1 和第 2 时。

为了解决这个问题,指向 Test.Add(...) 的指针被放入 _methodPtrAux_target 存储 addDelegate 本身,而 _methodPtr 包含指向称为 "shuffle thunk" 的特殊方法的指针。调用Invoke(...)时,shuffle thunk将"shuffling"参数处理到合适的位置,然后根据_methodPtrAux.

中存储的地址调用真正的方法

Invoke(...) 总是做同样的事情当然可以从运行时的角度更简单地调用委托,但会导致静态方法的(打开)委托比静态方法的(关闭)委托稍微慢一些由于 运行 shuffle thunk 首先的开销,实例方法。