委托上的 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 字段加载到this,Add(...) 方法的地址从@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 的内容将位于第一个参数位置,a 和 b 将位于第二个和第三个参数位置,而它们需要位于第一个和第二个参数位置。
为了解决这个问题,将指向Test.Add(...) 的指针放在_methodPtrAux 中,_target 存储addDelegate 本身,_methodPtr 包含一个指向称为“shuffle thunk”的特殊方法的指针.当Invoke(...) 被调用时,shuffle thunk 会将参数“洗牌”到合适的位置,然后根据存储在_methodPtrAux 中的地址调用真正的方法。
让Invoke(...) 总是做同样的事情,当然从运行时的角度来看,调用委托更简单,但可能导致(打开)静态方法的委托比(关闭)实例方法的委托稍慢,因为首先运行 shuffle thunk 的开销。