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

.NET 繼承本質深度解析:從 IL、MethodTable、虛分派到介面映射表

     編輯:關於.NET

網路上關於 C# virtual/override/new 的語法討論眾多,卻少從 CLR 執行層面說明。本文以 SOS 除錯器、IL 反組譯、MethodTable 結構為證,揭示:所有實例方法呼叫均為 callvirt,唯基類呼叫用 call;override 本質是子類 MethodTable 覆寫虛方法槽位;介面呼叫透過全域介面映射表分派。

關鍵觀察 1:呼叫指令與虛分派

// C#
A a = new A(); a.Foo(); a.Boo();
A b = new B(); b.Foo(); b.Boo();

// IL:皆為 callvirt
IL_0007: callvirt instance void A::Foo()
IL_000d: callvirt instance void A::Boo()
IL_0019: callvirt instance void A::Foo()
IL_001f: callvirt instance void A::Boo()

// B.Foo() 覆寫並呼叫 base.Foo()
IL_000b: call instance void A::Foo()  // 唯一 call,防無限遞迴

結論:編譯器只看**靜態型別**(變數宣告型別)產生 callvirt,JIT 時才依物件真實 MethodTable 解析槽位位址。非虛方法同樣用 callvirt,JIT 內部轉為直接 call 並插入 null 檢查(cmp dword ptr [ecx], ecx)。

關鍵觀察 2:MethodTable 佈局與虛方法槽位

物件前 4 bytes 指向 MethodTable。!dumpmt -md 顯示:

// A 的 MethodTable
... 
00973148 009730d8 JIT A.Foo()   // 第 5 槽位
00973138 009730d0 JIT A.Boo()

// B 的 MethodTable
...
009731d0 00973178 NONE B.Foo()  // 同樣第 5 槽位,覆蓋 A.Foo
009731e0 00973180 JIT B..ctor()

override 只是將父類虛方法槽位的位址替換為子類實作。若子類用 new virtual,則在父類虛方法之後**新增**槽位。

關鍵觀察 3:JIT 產生的組合語言驗證

a.Foo():  mov ecx, edi          // this
          mov eax, [ecx]        // MethodTable
          call [eax+38h]        // 偏移 0x38 = 第 5 槽位 * 8

b.Foo():  mov ecx, ebx
          mov eax, [ecx]
          call [eax+38h]        // 相同偏移,但指向 B.Foo()

非虛方法 Boo():

mov ecx, edi
cmp [ecx], ecx          // null 檢查
call 0xFF9430C8         // 直接呼叫固定位址

介面呼叫:全域介面映射表

介面方法不在類別 MethodTable 固定槽位,CLR 維護 **Global Interface Map Table**,依 (介面型別, 實作類別) 查找實際 MethodDesc。範例:

interface IC { void Test(); }
class C : IC { public void Test() { ... } }  // IL: virtual final
class D : C { void Test() { ... } }          // new slot, non-virtual

IC ic = new D(); ic.Test(); // 輸出 "C::Test()"

因 C.Test 為 final,D.Test 不進介面映射表。若 D 也實作 IC 或 C.Test 移除 final,則 D.Test 會登入映射表並被呼叫。

總結

  • callvirt + MethodTable 槽位 = 虛分派核心;
  • override = 槽位覆寫;new = 新增槽位;
  • 介面分派 = 全域映射表查找,與類別繼承獨立;
  • 基類呼叫 base.Foo() 必須用 call 避免遞迴。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved