为什么在 IL 代码中找不到委托的调用方法正文?



我为我的TestDelegate反编译源代码

public delegate int TestDelegate(int a, int b);

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

.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 显示调用方法调用,我找不到它。这到底是怎么回事?

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

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

来自委托(C# 编程指南(:

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

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

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

当然,询问它如何在"引擎盖下"工作是合理的,尽管答案非常复杂,因为它取决于委托要调用的方法类型(例如静态与实例方法,虚拟与非虚拟等(以及委托是"开放"还是"封闭"。

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

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

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字段加载到this中,Add(...)方法的地址从_methodPtr字段加载并被调用,因此几乎与直接调用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将位于第二个和第三个参数位置, 当他们需要在 1 日和 2 号时。

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

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

最新更新