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

[你必須知道的.NET]第二十三回:品味細節,深入 .NET 型別建構器

     編輯:關於.NET

引言

在 Artech 大大的文章《關於 Type Initializer 和 BeforeFieldInit 的問題,看看大家能否給出正確的解釋》中,我們看到了一個關於型別建構器呼叫時機的有趣範例,並引發了對 beforefieldinit 影響型別建構器執行時機的探討。這為理解型別建構器提供了絕佳的實戰體驗。

本文作為補充,將從基礎出發層層深入,針對原文未詳細闡述的概念與原理進行必要補充——例如更全面地認識型別建構器、認識 BeforeFieldInit。在此基礎上,探討型別建構器的實踐應用,並期望能解答原文範例的執行結果。

認識物件建構器與型別建構器

在 .NET 中,類別的初始化過程在建構器中進行。依據建構成員的類型,分為型別建構器(.cctor)與物件建構器(.ctor),兩者分別為 IL 程式碼中的指令表示。.cctor 無法被直接呼叫,其呼叫規則正是本文重點;而 .ctor 會在型別實例化時自動被呼叫。

為了探討型別建構器,我們先實作一個包含一般建構器與靜態建構器的簡單類別定義:

// Release : code01, 2008/11/02
// Author : Anytao, https://www.anytao.com
public class User
{
    static User()
    {
        message = "Initialize in static constructor.";
    }
    public User()
    {
        message = "Initialize in normal constructor.";
    }
    public User(string name, int age)
    {
        Name = name;
        Age = age;
    }
    public string Name { get; set; }
    public int Age { get; set; }
    public static string message = "Initialize when defined.";
}

使用 ILDasm.exe 反編譯可清晰看到對應的型別建構器與物件建構器。

物件建構器(.ctor)

在產生的 IL 程式碼中可看到對應的 ctor,型別實例化時會執行對應建構器進行初始化。關於實例化的複雜執行順序,可參閱《你必須知道的.NET》第 7.8 節「動靜之間:靜態和非靜態」,本文不再贅述。

型別建構器(.cctor)

用於執行靜態成員的初始化。在 .NET 中,型別在兩種情況下會發生 .cctor 的呼叫:

  • 為靜態成員指定初始值:例如上例中只有靜態成員初始化、無靜態建構函數時,.cctor 的 IL 實作為單純的靜態欄位賦值。
  • 實作顯式的靜態建構函數:當存在靜態建構函數時,會先執行靜態成員初始化,再執行靜態建構函數初始化,.cctor 的 IL 程式碼會依序包含這兩部分操作。

靜態建構函數的基本規則包括:

  • 必須為靜態無參建構函數,一個類別只能有一個。
  • 只能對靜態成員進行初始化。
  • 靜態無參建構函數可與非靜態無參建構函數共存,差異在於執行時機。

深入執行過程

因為型別建構器的特性,.cctor 的呼叫時機並非確定概念。由於型別建構器皆為 private,使用者無法顯式呼叫。.NET 中主要有兩種方案:

  • precise 方式
  • beforefieldinit 方式

兩者執行差異主要在於是否實作了顯式的靜態建構函數:若實作了顯式靜態建構函數,則採 precise 方式;若未實作,則採 beforefieldinit 方式。

3.1 precise 方式

當類別顯式實作靜態建構函數時:

public class User
{
    static User()
    {
        message = "Initialize in static constructor.";
    }
    public static string message = "Initialize when defined.";
}

對應 IL 中類別宣告不含 beforefieldinit 標記。CLR 必須在第一次存取該型別的靜態成員或建立實例之前,精確執行型別建構器。

3.2 beforefieldinit 方式

當類別未實作顯式靜態建構函數,僅有靜態欄位初始化時:

public class User
{
    public static string message = "Initialize when defined.";
}

對應 IL 中類別宣告包含 beforefieldinit 標記:

.class public auto ansi beforefieldinit User

3.3 差異分析

從 IL 實作來看,兩者內部邏輯基本相同,差異在於中繼資料宣告上是否標記 beforefieldinit。Scott Allen 解釋:beforefieldinit 讓 CLR 擁有在任何時候執行 .cctor 的授權,只要在第一次存取型別靜態欄位前執行即可。

  • precise 方式:CLR 必須在第一次存取靜態/實例成員剛好之前執行 .cctor,語意精確。
  • beforefieldinit 方式:CLR 可在任意時機執行 .cctor,一定程度優化效能,較 precise 方式更高效。

值得注意的是,當多個 beforefieldinit 型別建構器共存時,CLR 無法保證它們的執行順序,實務編碼應盡量避免此情況。

回歸問題,必要小結

針對 Artech 原文中的範例分析:

  • 顯式實作靜態建構函數:採 precise 方式,型別建構器在引用靜態成員剛好之前執行,無論 Main 中是否宣告 string field = Foo.Field;,結果不受影響。
  • 未顯式實作靜態建構函數:採 beforefieldinit 方式,CLR 可在靜態成員引用或實例建立之前的任意時刻執行。Debug 環境下呼叫時機看似不按常理,但 Release 優化模式下完全符合 beforefieldinit 語意定義。
  • 靜態成員繼承情況:結果完全遵守規範。但不建議子類別直接呼叫父類別靜態成員,因繼承機制中靜態方法本質上存在不足,以規範約束較佳。

C# 規格中對 beforefieldinit 的控制引起關注:一方面可優化效能,但以顯式/隱式實作靜態建構函數來控制不夠直觀,期待未來版本能以屬性宣告方式控制。另一方面,兩個型別建構器相互引用時,CLR 無法保證呼叫順序,開發者應避免在型別建構器中編寫依賴順序的業務邏輯。

結論

本文補充了 Artech 的問題,延續了《你必須知道的.NET》第 7.8 節對型別建構器的探討,以更全面視角闡釋此議題。雖未 100% 掌握 Debug 模式下 CLR 呼叫行為,但深入細節讓我們對語言內部有更多掌控與理解,這正是開始。

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