【问题标题】:Why can't I find the Invoke method body of a delegate in IL code?为什么在 IL 代码中找不到委托的 Invoke 方法体?
【发布时间】:2019-12-26 07:30:33
【问题描述】:

我为 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)

Generating IL 显示 Invoke 方法调用,我找不到。到底发生了什么?

【问题讨论】:

    标签: c# delegates cil


    【解决方案1】:

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

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

    来自Delegates (C# Programming Guide)

    委托是一种表示对具有特定参数列表和返回类型的方法的引用的类型。实例化委托时,可以将其实例与具有兼容签名和返回类型的任何方法相关联。您可以通过委托实例调用(或调用)该方法。

    【讨论】:

    • 感谢您的回答,但是当我调用 Invoke() 时会发生什么?换句话说,委托中的方法是如何被调用的?
    • 如果委托被分配给一个方法,它会调用委托被分配给的方法。如果没有,您将收到 NullReferenceException 以尝试调用空委托。
    • 我明白了,我想知道它是如何工作的。调用方法的代码是怎么写在字节码上的?
    • 字节码? IL 与字节码相差甚远……就我个人而言,我不记得我上次关心 IL 级别发生的事情是什么时候,更不用说更低级别了。我不确切知道运行时如何管理委托,而且我真的不需要知道这一点才能知道如何与委托合作——就像我不需要知道内燃机是如何工作的就知道如何开车。
    • 这个答案没有多大意义 - 是的,委托是对方法的引用,而不是方法本身,而是Invoke() 方法,所以问关于它的实现是一个完全合理的问题。
    【解决方案2】:

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

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

    当然可以合理地询问它是如何“在幕后”工作的,尽管答案非常复杂,因为它取决于您的委托要调用的方法的 种类 (例如静态与实例方法、虚拟与非虚拟等)以及委托是“打开”还是“关闭”。

    虽然“打开”和“关闭”不是我们通常在委托上下文中遇到的术语,但含义相对简单 - “关闭”委托存储将在以下情况下调用的方法的第一个参数静态方法,或者在实例方法的情况下将调用该方法的实例(即this),而“开放”委托则没有。如果您有兴趣,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(...) 方法的地址从@987654353 加载@ 字段,并被调用,因此几乎与直接调用 testInstance.Add(...) 完全一样。

    对于静态开放委托,您可以执行以下操作:

    public class Test
    {
        public static int Add(int a, int b)
        {
            return a + b;
        }
    }
    
    var addDelegate = new TestDelegate(Test.Add);
    

    这里,addDelegate 是一个静态开放委托,是一个稍微复杂的场景。在这种情况下,没有实例,因为Test.Add(...) 是静态的,但由于Invoke(...) 始终以相同的方式工作,如果要在_methodPtr 中存储指向Test.Add(...) 的指针,我们将遇到问题,因为参数会在错误的位置 - _target 的内容将位于第一个参数位置,ab 将位于第二个和第三个参数位置,而它们需要位于第一个和第二个参数位置。

    为了解决这个问题,将指向Test.Add(...) 的指针放在_methodPtrAux 中,_target 存储addDelegate 本身,_methodPtr 包含一个指向称为“shuffle thunk”的特殊方法的指针.当Invoke(...) 被调用时,shuffle thunk 会将参数“洗牌”到合适的位置,然后根据存储在_methodPtrAux 中的地址调用真正的方法。

    Invoke(...) 总是做同样的事情,当然从运行时的角度来看,调用委托更简单,但可能导致(打开)静态方法的委托比(关闭)实例方法的委托稍慢,因为首先运行 shuffle thunk 的开销。

    【讨论】:

    • @EvgeniyTerekhin - 您可能想看看 .NET Core 运行时中的委托实现,从 SetupShuffleThunk 方法开始。最终,shuffle thunk 是由平台特定的EmitShuffleThunk 方法之一在运行时生成的本机代码(例如,this one 用于 x86/x64)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多