實例構造方法

構造方法(Constructor)是型別在建立實例時執行的初始化邏輯。在 metadata / IL 裡,它會以 .ctor 的形式出現。

public SomeType() { }
public SomeType(int value) { }
// ildasm 中常見的樣子
.ctor : void()
.ctor : void(int32)

對參考型別來說,一次正常的 new 大致會經過這些步驟:

  1. 配置物件所需記憶體。
  2. 將實例字段先清成 0 / null
  3. 先執行基底型別建構流程。
  4. 再執行目前型別自己的建構邏輯。

這也是為什麼「欄位初始值」和「建構函式內容」其實都只是初始化流程的一部分。

預設構造方法與可見性

如果類別沒有明確定義任何實例構造方法,C# 會幫你補一個預設無參數構造方法。

但這個規則有幾個邊界:

  • 一旦你自己宣告了任一建構函式,編譯器就不再自動補無參數版本。
  • abstract 類別的預設建構函式語意上更接近 protected
  • static class 不會有實例構造方法,因為它根本不能被實例化。

另外,構造方法本身不會被繼承,也不能標記成:

  • virtual
  • override
  • abstract
  • sealed

原因很簡單:建構流程本來就不是給多型覆寫用的。

基底構造方法永遠先跑

衍生類別的建構過程,一定會先走到基底型別。

如果你沒有顯式寫出 base(...),編譯器就會嘗試幫你補上一個對基底無參數建構函式的呼叫。這也代表:

  • 如果基底沒有無參數建構函式,你通常得顯式指定 base(...)
  • 想看懂物件初始化狀態,不能只看目前這一層類別

最終所有參考型別的建構鏈都會一路回到 System.Object

不要在構造方法裡呼叫可覆寫方法

這是很老但仍然常見的坑。因為在基底建構函式執行期間,衍生類別自己的字段可能還沒初始化完,如果這時基底建構函式呼叫了一個可被 override 的方法,實際執行的可能是衍生類別版本,而它讀到的狀態往往還不完整。

換句話說,下面這種設計風險很高:

public class Base
{
    public Base()
    {
        Initialize();
    }

    protected virtual void Initialize() { }
}

安全做法通常是:

  • 把初始化邏輯收斂在私有方法
  • 或改成建構完成後由外部顯式呼叫初始化步驟

有些情況下,物件可以繞過構造方法

大多數時候我們透過 new 建立物件,但 CLR 並不是每次都一定呼叫實例構造方法。

幾個典型例子:

  • Object.MemberwiseClone():建立淺拷貝
  • 某些序列化 / 反序列化流程
  • FormatterServices.GetUninitializedObject(...) 這類低階 API

這類機制通常會直接配置物件,跳過正常建構邏輯。因此如果某個型別嚴重依賴 constructor 保證不變式,就要知道這份保證不是在所有低階場景都自動成立。

值型別的構造規則

值型別(struct)和參考型別不同。CLR 永遠知道如何產生一份「全 0 狀態」的值型別實例,這個能力不依賴你有沒有自己寫建構函式。

internal struct BaseValue
{
    public int x;
    public int y;
}

理解值型別建構時,先抓住兩個核心事實:

  • default(T) 一定存在,並代表欄位全為預設值的實例。
  • 你自己寫的值型別建構函式,重點是把狀態整理成你需要的有效值。

在較新的 C# 版本中,struct 建構規則比早期寬鬆,但無論語法版本如何,一個原則都沒變:當建構函式返回前,實例字段必須處於可用且可解釋的狀態。

internal struct Point
{
    public int x;
    public int y;

    public Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
}

類型構造方法

除了實例構造方法,型別還可以有類型構造方法,也就是 static constructor。在 IL 裡它叫 .cctor

static SomeType() { }
// ildasm 中常見的樣子
.cctor : void()

它的用途很單純:初始化靜態字段,或執行只需要做一次的型別級準備工作。

幾個基本特徵

  • 一個型別最多只有一個 static constructor。
  • 它沒有參數。
  • 在 C# 語法上它沒有存取修飾詞,本質上由執行時控制呼叫時機。
  • 不能手動直接呼叫,只能等待 CLR 在型別首次真正被使用前觸發。
internal sealed class SomeRefType
{
    private static int s_X = 5;
    private static int s_Y;

    static SomeRefType()
    {
        s_Y = 10;
    }
}

類型構造方法的觸發與執行保證

只要型別定義了 .cctor,JIT 與 CLR 就會在適當時機插入檢查,確保它在每個 AppDomain 中最多只執行一次。

這帶來兩個重要結論:

  • static constructor 的執行具備執行時層級的一次性保證。
  • 這個過程是執行緒安全的,不需要你自己再加一層鎖來保證「只初始化一次」。

但不要因此把它當成萬用初始化中心。特別要避免:

  • 在多個型別的 static constructor 中互相依賴
  • 做過重的 I/O 或容易失敗的外部初始化
  • 把恢復成本很高的流程塞進型別第一次使用的路徑

沒有靜態析構方法

型別可以有 static constructor,但沒有對應的 static destructor。CLR 不支援靜態 Finalize

如果真的需要在 AppDomain 卸載或程序結束時做清理,通常會改用:

  • IDisposable
  • 應用程式生命週期事件
  • AppDomain.DomainUnload

小結

構造方法這一章真正值得留下來的重點其實不多:

  • 參考型別的建構流程一定先經過基底型別。
  • 構造方法不是多型擴展點,不要拿它做虛方法分派。
  • 低階機制有可能繞過實例構造方法,別把它當成唯一防線。
  • 值型別永遠有一個可用的預設狀態。
  • static constructor 適合做一次性的型別初始化,但不要把它變成副作用黑盒。

參考資料

  • 《CLR via C#》