結構 是 C# 的實值型別建構單元,適合表示小型、不可變、資料導向的型別。結構儲存在堆疊(或內含物件內部),採複製語意,無繼承、無解構子、無參考身分。正確使用結構能顯著提升效能、減少 GC 壓力;濫用則導致過度複製、封箱開銷。
| 特性 | 結構 | 類別 |
|---|---|---|
| 型別分類 | 實值型別 | 參考型別 |
| 記憶體配置 | 堆疊 / 內嵌 | 堆積 |
| 指派行為 | 複製值 | 複製參考 |
| 繼承 | 不可繼承類別/結構,可實作介面 | 單一繼承,可實作介面 |
| 預設值 | 所有欄位為 0/null | null |
| 建構子 | 不可定義無參數建構子 (C# 10 前) | 可定義任意建構子 |
| 解構子 | 不支援 | 支援 |
| Null | 不可為 null (除非用 Nullable<T>) | 可為 null |
| 虛擬方法 | 不可 virtual/abstract/protected | 支援 |
| 封箱/解箱 | 轉為 object/介面時發生 | 天然為參考 |
public readonly struct Point3D { // readonly struct (C# 7.2+) 確保不可變
public double X { get; }
public double Y { get; }
public double Z { get; }
public Point3D(double x, double y, double z) {
X = x; Y = y; Z = z;
}
public double DistanceFromOrigin => Math.Sqrt(X*X + Y*Y + Z*Z);
public override string ToString() => $"({X}, {Y}, {Z})";
// 運算子多載
public static Point3D operator +(Point3D a, Point3D b) =>
new Point3D(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
}
C# 10 前:結構不能宣告無參數建構子,預設建構子將所有欄位歸零。C# 10+ 允許自訂無參數建構子,但必須初始化所有欄位:
public struct Color {
public byte R, G, B, A;
public Color() { R = G = B = 0; A = 255; } // C# 10+ 合法
}
實例欄位不可在宣告處初始化(C# 10 前):
public struct Bad {
// public int Value = 5; // 編譯錯誤
public int Value; // 必須在建構子初始化
}
C# 7.2+ readonly struct 強制所有實例成員為 readonly,編譯器保證不可變性,避免防禦性複製:
public readonly struct ImmutablePoint {
public int X { get; }
public int Y { get; }
public ImmutablePoint(int x, int y) { X = x; Y = y; }
// 編譯器檢查:不可有 set 存取子、不可修改欄位
}
C# 7.2+ ref struct 僅能分配在堆疊,不可用於堆積(陣列元素、類別欄位、封箱、非同步方法、迭代器):
public ref struct SpanWrapper {
public Span Data; // 僅限堆疊
// 不可作為類別欄位、陣列元素、封箱
}
典型用途:Span<T>、ReadOnlySpan<T>、ref struct 狀態機。
Point[] 連續記憶體、無額外參考開銷、Cache 友善。List<object>、介面變數、非泛型集合。public readonly struct Vector3 {
public readonly float X, Y, Z;
public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
public float Length => MathF.Sqrt(X*X + Y*Y + Z*Z);
public float LengthSquared => X*X + Y*Y + Z*Z;
public static Vector3 operator +(Vector3 a, Vector3 b) =>
new Vector3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
public static Vector3 operator -(Vector3 a, Vector3 b) =>
new Vector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
public static Vector3 operator *(Vector3 v, float s) =>
new Vector3(v.X * s, v.Y * s, v.Z * s);
public static float Dot(Vector3 a, Vector3 b) => a.X*b.X + a.Y*b.Y + a.Z*b.Z;
public static Vector3 Cross(Vector3 a, Vector3 b) => new Vector3(
a.Y*b.Z - a.Z*b.Y,
a.Z*b.X - a.X*b.Z,
a.X*b.Y - a.Y*b.X
);
}
// 使用:零配置、零 GC
var particles = new Vector3[1_000_000];
for (int i = 0; i < particles.Length; i++) {
particles[i] = new Vector3(random.NextSingle(), random.NextSingle(), random.NextSingle());
}
// 批次運算:極佳快取局部性
for (int i = 0; i < particles.Length; i++) {
particles[i] = particles[i] * 0.99f + new Vector3(0, -9.8f, 0) * 0.016f;
}
呼叫非 readonly 結構的 readonly 欄位/屬性時,編譯器會建立副本防止意外修改:
public struct Counter { public int Value; public void Increment() => Value++; }
public class Holder { public readonly Counter Counter = new Counter(); }
var h = new Holder();
h.Counter.Increment(); // 編譯器警告:在 readonly 欄位上呼叫非 readonly 方法,會複製
Console.WriteLine(h.Counter.Value); // 0 (修改的是副本)
// 解決:用 readonly struct 或移除 readonly 欄位修飾詞
// 錯誤:每次迴圈封箱
foreach (object o in myStructs) { Console.WriteLine(o.ToString()); }
// 正確:泛型避免封箱
foreach (var s in myStructs) { Console.WriteLine(s.ToString()); }
A: 本身不可,但可用 Nullable<T> / T? 包裝:int? x = null; DateTime? d = null;。
A: 會。透過介面型別存取結構實例會發生封箱。泛型約束 where T : IInterface 可避免封箱。
A: 允許自訂預設值(如 Color() { A=255 }),但new T() 和 default(T) 行為不同:前者呼叫無參數建構子,後者仍歸零。