在編寫動態方法時,OpCodes.Call 與 OpCodes.Callvirt 經常混淆。MSDN 文檔說「Callvirt 對對象調用後期綁定方法」,其實指的就是 虛方法動態分派。本文通過 DynamicMethod 實戰驗證兩者在繼承體系中的具體表現差異。
public class Animal
{
public virtual void Speak() => Console.WriteLine("Animal.Speak");
}
public class Cat : Animal
{
public override void Speak() => Console.WriteLine("Cat.Speak");
}
public class Dog : Animal
{
public override void Speak() => Console.WriteLine("Dog.Speak");
}
private delegate void SpeakDelegate(Animal animal);
static void Main()
{
// 1. 使用 Call
var dmCall = new DynamicMethod("SpeakCall", null, new[] { typeof(Animal) }, typeof(Program).Module);
var ilCall = dmCall.GetILGenerator();
ilCall.Emit(OpCodes.Ldarg_0);
ilCall.Emit(OpCodes.Call, typeof(Animal).GetMethod("Speak")); // 靜態綁定
ilCall.Emit(OpCodes.Ret);
var actCall = (SpeakDelegate)dmCall.CreateDelegate(typeof(SpeakDelegate));
// 2. 使用 Callvirt
var dmVirt = new DynamicMethod("SpeakCallvirt", null, new[] { typeof(Animal) }, typeof(Program).Module);
var ilVirt = dmVirt.GetILGenerator();
ilVirt.Emit(OpCodes.Ldarg_0);
ilVirt.Emit(OpCodes.Callvirt, typeof(Animal).GetMethod("Speak")); // 動態分派
ilVirt.Emit(OpCodes.Ret);
var actVirt = (SpeakDelegate)dmVirt.CreateDelegate(typeof(SpeakDelegate));
Console.WriteLine("SpeakWithCall:");
actCall(new Animal());
actCall(new Cat());
actCall(new Dog());
Console.WriteLine("SpeakWithCallvirt:");
actVirt(new Animal());
actVirt(new Cat());
actVirt(new Dog());
}
SpeakWithCall:
Animal.Speak
Animal.Speak
Animal.Speak
SpeakWithCallvirt:
Animal.Speak
Cat.Speak
Dog.Speak
| 指令 | 綁定方式 | 多態支援 | 適用場景 |
|---|---|---|---|
Call | 編譯期靜態綁定 (Early Binding) | ❌ 始終調用聲明類型的實現 | 非虛方法、構造函數、基類方法顯式調用 (base.Method()) |
Callvirt | 運行期動態分派 (Late Binding) | ✅ 根據實例真實類型調用重寫版本 | 虛方法、介面方法、委托調用 |
Callvirt 會隱式插入 null 檢查,若對象為 null 拋 NullReferenceException;Call 不會。這也是 C# 編譯器對非虛方法偶爾仍發 Callvirt 的原因——為了保證空安全。
virtual/abstract/interface 方法 → Callvirtstatic/non-virtual/constructor → CallMethodInfo 指定基類)理解這一點,動態代理、AOP 攔截器、序列化庫的 IL 生成邏輯就清晰多了。