從C#方法表看透方法呼叫的本質

2020-07-16 10:04:47
類、結構和介面可以擁有自己的方法。委託作為一個特殊的類,也有自己的方法。

參照型別通過型別物件指標可以找到型別方法表,從而呼叫方法。

對於值型別,也有方法表(任何型別都有方法表),但值型別的範例沒有型別物件指標指向它,需要存取型別後設資料獲得方法表。

型別方法表是在型別載入的過程中建立的,和型別物件的建立位於相同的階段。

我們使用下面的範例型別:
interface ITest
{
    string interfaceMethod();
}
class FatherClass : ITest
{
    public static int i = 1;
    public string interfaceMethod()
    {
        Console.WriteLine("繼承介面的方法");
        return "test";
    }
    public int NormalMethod(int a)
    {
        return a + 1;
    }
    public int NormalMethod2(int a)
    {
        return a + 2;
    }
    public int NormalMethod3(int a)
    {
        return a + 3;
    }
    public virtual void VirtualMethod1()
    {
        Console.WriteLine("VirtualMethod1");
    }
    public virtual void VirtualMethod2()
    {
        Console.WriteLine("VirtualMethod2");
    }
}
主程式:
static void Main(string[] args)
{
    var a = new FatherClass();
    Console.WriteLine("沒呼叫方法");
    Console.ReadKey();

    a.NormalMethod(100);
    Console.WriteLine("呼叫方法");
    Console.ReadKey();
}

方法表

型別物件中最重要的部分無疑是方法表,它是類載入過程中生成的。

在上面的程式碼中,範例型別含有三個普通方法:兩個虛方法,一個來自介面繼承的方法,以及範例建構函式 .ctor 和靜態建構函式 .cctor (因為包含了對靜態欄位的賦值),可以在方法表中找到它們。

另外,方法表還含有型別所有父類別的虛方法,父類別的其他方法不出現在子類的方法表中。

方法表的排列順序嚴格按照方法定義的順序,並從最高輩分開始往下排。因此,範例型別的方法表含有下面的成員(還有其他成員):
  • Object 的四個非虛方法
  • 自己繼承自介面的方法
  • 自己的虛方法
  • 兩個建構函式
  • 自己的普通方法

這些成員組成了方法表的一部分——方法槽表(method slot table)。

方法槽表按照如下順序排列:繼承的虛方法、自己繼承自介面的方法、自己的虛方法、建構函式、自己的實體方法、自己的靜態方法(不同版本的 CLR,順序可能不同)。

我們可以看到 Object 類中有一些方法不在其中,這是因為它們不是虛方法。

方法槽表的每一個成員都包含著另一個表,即方法描述(MethodDesc)的其中一個位置,和那個表實現一一對應關係。

方法呼叫

方法的呼叫是 .NET 框架中最有趣的功能之一。簡單來說,方法的呼叫是一個路由的過程。

我們通過棧上的參照找到型別的方法表指標,它又指向方法表的開頭。

通過確定的偏移量,CLR 馬上就可以定位到介面虛表的開頭,或者方法槽錶的開頭。

對前者來說,假設要呼叫的方法 X 位於介面 Y 中,接下來的事情就是查詢到指向 Y 的方法表在介面虛表中的位置(不會順序尋找,因為每個介面的偏移量都已經被索引好了)。

然後,就可以定位到 Y 的方法表,之後,再次通過偏移量跳轉到 Y 的方法槽錶的開頭,最後就和後者相同。

對於後者來說,CLR 在方法槽表中找到方法,然後找到存根例程,根據它後面的 jmp 指令,就可以被引導到 JIT 編譯器程式碼或者機器碼,從而繼續方法的呼叫。

而實際上,每個方法都有自己的偏移量(在型別載入時,方法表的順序就已經確定了,偏移量也就可以被計算出來了)。

因此,CLR 是不會一個一個地尋找的,它總是一步到位。

1) 方法的反射呼叫

我們可以想象,如果直接存取型別物件獲得了某個方法的地址,並傳入必須的引數,那麼我們是不是就可以進行方法呼叫了呢?答案是肯定的。

這種型別的方法呼叫甚至不需要一個對應型別的範例,因為我們是直接從型別物件(後設資料的一部分)出發的,而傳統的方式是從棧上的參照(範例)出發的,這樣的做法就叫做反射(reflection),如下圖所示。

方法的反射調用