結構體與類別是 C# 建立自訂型別的兩大基石。雖然語法相似,但語意、記憶體行為、效能特徵截然不同。本文從 CLR 層面深度解析,提供實務選擇決策樹。
| 特性 | 結構體 | 類別 |
|---|---|---|
| 型別分類 | 值型別 | 參考型別 |
| 記憶體配置 | 堆疊 或 內嵌於父物件 | Managed Heap (GC 管理) |
| 變數賦值 | 複製整個實例 (深拷貝) | 複製參考 (淺拷貝) |
| 預設值 | 所有欄位為 0/null/false | null |
| 繼承 | 不可繼承類別/結構體,隱式 sealed | 單一繼承,可標記 sealed |
| 介面實作 | ✅ 可實作介面 | ✅ 可實作介面 |
| 建構函式 | 無預設無參建構函式,不可定義無參 ctor | 有預設無參 ctor,可定義任意 ctor |
| 解構函式 | ❌ 不支援 | ✅ Finalizer (~ClassName) |
| 欄位初始化 | 宣告時不可初始化 | 可宣告時初始化 |
| protected | ❌ 不支援 (不可繼承) | ✅ 支援 |
| null 可指派 | 需 Nullable<T> 或 T? | 原生支援 |
| 裝箱/拆箱 | 轉為 object/介面時發生 | 無 (本身即參考) |
| 傳遞給方法 | 預設傳值 (複製),可用 ref/in/out | 傳參考 (複製參考) |
類別實例:
Stack: Heap:
myClass ──────┐ ┌─────────────────┐
└──────────►│ Object Header │
│ Sync Block Index│
├─────────────────┤
│ Field1 (int) │
│ Field2 (string) │──► "Hello" (另一堆疊物件)
└─────────────────┘
結構體實例 (區域變數):
Stack:
myStruct ┌─────────────────┐
│ Field1 (int) │
│ Field2 (string) │──► "Hello" (字串仍在堆疊)
└─────────────────┘
結構體陣列:
Stack: Heap:
arr ──────┐ ┌─────────────────┐
└──────────────►│ Array Header │
├─────────────────┤
│ [0] Field1 │
│ [0] Field2 │──► "Hello"
│ [1] Field1 │
│ [1] Field2 │──► "World"
└─────────────────┘
public struct PointStruct
{
public int X, Y;
public override string ToString() => $"({X},{Y})";
}
public class PointClass
{
public int X, Y;
public override string ToString() => $"({X},{Y})";
}
public static void Demo()
{
// 1. 賦值行為
PointStruct s1 = new PointStruct { X = 1, Y = 2 };
PointStruct s2 = s1; // 複製值
s2.X = 10;
Console.WriteLine($"s1: {s1}, s2: {s2}"); // s1: (1,2), s2: (10,2) - 獨立
PointClass c1 = new PointClass { X = 1, Y = 2 };
PointClass c2 = c1; // 複製參考
c2.X = 10;
Console.WriteLine($"c1: {c1}, c2: {c2}"); // c1: (10,2), c2: (10,2) - 共享
// 2. 方法傳遞
ModifyStruct(s1); // 傳值,複製
Console.WriteLine($"After ModifyStruct: {s1}"); // (1,2) 不變
ModifyClass(c1); // 傳參考
Console.WriteLine($"After ModifyClass: {c1}"); // (10,2) 變了
}
static void ModifyStruct(PointStruct p) { p.X = 99; }
static void ModifyClass(PointClass p) { p.X = 99; }
| 操作 | 結構體 (16 bytes) | 類別 (16 bytes + 參考) |
|------------------------|-------------------|------------------------|
| 實例化 (100萬次) | ~5 ms | ~15 ms (含 GC 壓力) |
| 陣列配置 (100萬) | 連續記憶體 16MB | 陣列 8MB + 物件 16MB |
| 陣列遍歷存取 | 極快 (CPU Cache 友善) | 較慢 (指標追蹤、Cache Miss) |
| 傳遞給方法 (by value) | 複製 16 bytes | 複製 8 bytes 參考 |
| 裝箱/拆箱 | 有開銷 | 無 |
| GC 掃描壓力 | 無 (非堆疊) | 有 |
Point[]、Vector3[] 連續記憶體、Cache 友善struct 對應 C/C++ 結構體、Win32 API 結構where T : struct、UnmanagedType| 特性 | 影響 |
|---|---|
readonly struct (C# 7.2) | 強制不可變,避免防禦性複製,編譯器優化 |
ref struct (C# 7.2) | 僅能棧上配置 (Span、Stackalloc 專用),不可裝箱、不可為欄位 |
record struct (C# 10) | 值型別記錄,自動實作 Equality、ToString、with 運算式 |
record class (C# 9) | 參考型別記錄,不可變預設、值語意相等 |
in 參數 (C# 7.2) | 唯讀傳參考,避免結構體複製,配合 readonly struct 最佳 |
with 運算式 | record 專用,非破壞性更新 |
開始 │ ├─ 需要繼承/多態? ──是──► class │ ├─ 需要為 null? ──是──► class (或 T?) │ ├─ 邏輯上是「值」而非「實體」? ──否──► class │ (如: 金額、座標、日期、顏色) │ ├─ 實例大小 > 24 bytes? ──是──► class │ ├─ 大量陣列/集合、高頻配置? ──是──► struct (或 readonly struct) │ ├─ 需要實作介面? ──是──► 兩者皆可 (struct 裝箱有開銷) │ └─ 預設選擇 ──► class (較安全、語意明確)
List.Count 取得複製品,修改不影響原列表 → readonly structforeach (var p in list) p.X = 1; 編譯錯誤 (唯讀) → 用 for 索引IEquatable 透過介面呼叫會裝箱 → 實作介面時考慮 readonly structin 參數或改 class結構體 = 值語意、堆疊/內嵌配置、複製語意、無繼承、適合小型高頻資料。類別 = 參考語意、堆疊配置、共享參考、支援繼承/多態、適合實體身分。現代 C# 以 class 為預設,readonly struct/record struct 解決傳統結構體痛點。理解記憶體模型,才能寫出高效能、正確的代碼。