首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

Emit 深度解析:Call 與 Callvirt 的多態行為差異

     編輯:關於.NET

在編寫動態方法時,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 的空檢查副作用

Callvirt 會隱式插入 null 檢查,若對象為 null 拋 NullReferenceException;Call 不會。這也是 C# 編譯器對非虛方法偶爾仍發 Callvirt 的原因——為了保證空安全。

Emit 選擇建議

  • 調用 virtual/abstract/interface 方法 → Callvirt
  • 調用 static/non-virtual/constructor → Call
  • 需顯式調用基類實現 → Call (配合 MethodInfo 指定基類)

理解這一點,動態代理、AOP 攔截器、序列化庫的 IL 生成邏輯就清晰多了。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved