struct 是 C# 值類型的基石,雖然日常開發多用 class,但理解結構體的**內存模型、複製語義、裝箱開銷**對編寫高性能、無 Bug 代碼至關重要。本文系統梳理結構體核心特征、陷阱與最佳實踐。
| 特性 | 說明 |
|---|---|
| 類型分類 | 值類型,繼承 System.ValueType → System.Object |
| 內存分配 | 棧上 (局部變數) 或 內聯在包含對象內 (字段/數組元素),無堆分配、無 GC 壓力 |
| 實例化 | 可 new,亦可無 new (需手動初始化所有字段) |
| 賦值語義 | 逐位複製 (Bitwise Copy),修改副本不影響原值 |
| 繼承限制 | 不可繼承類/結構,不可作為基類,隱式 sealed;可實現介面 |
| 構造函數 | 無參構造函數由編譯器自動生成 (零初始化),**不可自定義**;可定義帶參構造函數 (必須初始化所有字段) |
| 修飾符限制 | 不可 abstract/sealed/protected;成員不可 virtual/abstract (除覆寫 ValueType 方法) |
| 可空性 | 支援 Nullable<T> (即 T?) |
| 傳遞方式 | 預設按值傳遞;可用 ref/out/in 避免複製或啟用只讀傳遞 |
struct Point { public int X, Y; }
var dict = new Dictionary<string, Point>();
dict["p1"] = new Point { X = 1, Y = 2 };
// 錯誤嘗試修改字典內的值類型
dict["p1"].X = 10; // CS1612: 無法修改 'dict["p1"]' 的返回值
// 正確做法:取出 → 修改 → 存回
var pt = dict["p1"]; pt.X = 10; dict["p1"] = pt;
原因:索引器返回的是副本,修改副本無意義。此規則同樣適用於 List<Struct>[i]、foreach 迭代變數等。
object/介面。在堆上分配對象,複製值類型數據進入對象。object/介面 → 值類型。驗證類型後,複製數據出對象到棧上。
int i = 1;
object o = i; // 裝箱:堆上新建 Int32 對象,複製 1
i = 123; // 原值類型變數獨立變化
Console.WriteLine($"i={i}, o={o}"); // i=123, o=1 (互不影響)
避免策略:泛型集合 (List<T>)、泛型方法、Span<T>、介面約束 where T : struct。
new Struct() 調用編譯器生成的無參構造函數 (零初始化);new Struct(args) 調用自定義構造函數 (必須賦值所有字段)。default(Struct)、數組分配)。
struct S {
static S() { Console.WriteLine("Static Ctor"); }
public void M() {}
}
var arr = new S[100]; // 不觸發靜態構造函數
var s = new S(); // 觸發
s.M(); // 已觸發,不再執行
繼承需要類型對象頭存儲同步塊索引、方法表指針,並維護「繼承圖」元數據。對 Point 這樣 16 字節的小類型,元數據開銷可能超過數據本身。結構體犧牲繼承換取極致內存緊湊與訪問速度。
Point、Rectangle、Color、DateTime、Guid。Point[10000] 節省 10000 個對象頭 + GC 掃描開銷。readonly struct (C# 7.2):編譯器強制不可變,in 參數自動啟用只讀傳遞,避免防禦性複製。ref struct (C# 7.2):僅棧分配 (如 Span<T>),不能作為類字段、數組元素、裝箱、異步方法局部變數。with 表達式 (C# 9+):不可變結構體的非破壞性更新 var p2 = p1 with { X = 10 }。結構體不是「輕量級類」,而是值語義的第一公民。善用它,需理解「複製即賦值」、「棧上即生命週期」、「無繼承即零開銷」三大核心哲學。