實例構造方法
構造方法(Constructor)是型別在建立實例時執行的初始化邏輯。在 metadata / IL 裡,它會以 .ctor 的形式出現。
public SomeType() { }
public SomeType(int value) { }
// ildasm 中常見的樣子
.ctor : void()
.ctor : void(int32)
對參考型別來說,一次正常的 new 大致會經過這些步驟:
- 配置物件所需記憶體。
- 將實例字段先清成 0 /
null。 - 先執行基底型別建構流程。
- 再執行目前型別自己的建構邏輯。
這也是為什麼「欄位初始值」和「建構函式內容」其實都只是初始化流程的一部分。
預設構造方法與可見性
如果類別沒有明確定義任何實例構造方法,C# 會幫你補一個預設無參數構造方法。
但這個規則有幾個邊界:
- 一旦你自己宣告了任一建構函式,編譯器就不再自動補無參數版本。
abstract類別的預設建構函式語意上更接近protected。static class不會有實例構造方法,因為它根本不能被實例化。
另外,構造方法本身不會被繼承,也不能標記成:
virtualoverrideabstractsealed
原因很簡單:建構流程本來就不是給多型覆寫用的。
基底構造方法永遠先跑
衍生類別的建構過程,一定會先走到基底型別。
如果你沒有顯式寫出 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#》