首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 結構體 vs 類別深度解析:值型別語意、記憶體模型、效能與設計抉擇

     編輯:關於C語言

核心差異:值型別 vs 參考型別

特性結構體類別
型別分類值型別參考型別
記憶體配置堆疊 / 內嵌於包含型別受控堆
變數存儲直接包含資料存儲參考 (位址)
指派行為複製整個實例複製參考 (指向同一物件)
預設值全零位元 (欄位為 0/null/false)null
繼承不可繼承類別/結構體,可實作介面單一繼承,可繼承抽象/基底類別
解構函式無有 (Finalizer)
介面裝箱會發生裝箱無裝箱
null 狀態不可為 null (除非 Nullable<T>)可為 null

記憶體模型視覺化

結構體:內嵌配置

struct Point { public int X; public int Y; }

Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1; // 複製值:p2 獨立擁有 X=10, Y=20
p2.X = 99;
Console.WriteLine(p1.X); // 10 (未受影響)

// 陣列:連續記憶體區塊,無額外參考開銷
Point[] points = new Point[1000]; // 約 8KB (1000 * 8 bytes),單一陣列物件

類別:參考配置

class PointRef { public int X; public int Y; }

PointRef r1 = new PointRef { X = 10, Y = 20 };
PointRef r2 = r1; // 複製參考:r1、r2 指向同一堆疊物件
r2.X = 99;
Console.WriteLine(r1.X); // 99 (共享狀態)

// 陣列:參考陣列,每個元素獨立配置
PointRef[] refs = new PointRef[1000]; // 陣列 ~8KB + 1000 個獨立物件 ~24KB+

傳遞給方法:值 vs 參考

struct Point { public int X; public int Y; }
class PointRef { public int X; public int Y; }

void ModifyStruct(Point p) { p.X = 99; }      // 修改副本
void ModifyClass(PointRef p) { p.X = 99; }   // 修改原物件

Point s = new Point { X = 1 };
PointRef c = new PointRef { X = 1 };

ModifyStruct(s);
ModifyClass(c);

Console.WriteLine(s.X); // 1 (未變)
Console.WriteLine(c.X); // 99 (已變)

// ref/out 可改變結構體傳遞行為
void ModifyStructRef(ref Point p) { p.X = 99; }
ModifyStructRef(ref s);
Console.WriteLine(s.X); // 99 (原始值被修改)

效能分析:何時選結構體?

優勢

  • 零 GC 壓力:堆疊/內嵌配置,不增加堆疊物件計數
  • 快取友善:陣列連續記憶體,CPU 快取命中率高
  • 複製語意清晰:避免共享可變狀態 Bug
  • 介面呼叫無虛擬調度:直接呼叫 (除非裝箱)

劣勢

  • 大型結構體複製開銷:> 16 bytes 建議改類別 (經驗值)
  • 頻繁裝箱/拆箱:當作 object/介面使用時效能損失
  • 遞迴/樹狀結構不適用:無法自我參考 (需用類別或指標)
  • 不可為 null:需包裝 Nullable<T> 增加開銷

基準測試參考

// 16 bytes 結構體 vs 類別 (1000 萬次迴圈)
struct SmallStruct { public long A, B; }     // 16 bytes
class SmallClass { public long A, B; }       // 參考 + 物件

// 結構體陣列:~50ms
// 類別陣列:~120ms (含 GC)
// 大型結構體 (64 bytes) 複製:明顯慢於類別參考傳遞

設計決策流程圖

需要型別嗎?
├─ 是 → 類別 (參考語意、多型、身分識別、可為 null、繼承)
└─ 否 → 值型別語意?
      ├─ 不可變、小型 (<16-24 bytes)、邏輯值 (如 DateTime、Guid、錢幣)
      │   └─ 結構體
      ├─ 需要 null?
      │   ├─ 是 → 類別 或 Nullable<Struct>
      │   └─ 否 → 結構體
      ├─ 遞迴/圖狀結構?
      │   └─ 類別
      └─ 高頻介面轉換/裝箱?
          └─ 類別

進階:readonly struct (C# 7.2+)

// 編譯器強制不可變,避免防禦性複製
public readonly struct Vector3
{
    public double X { get; }
    public double Y { get; }
    public double Z { get; }

    public Vector3(double x, double y, double z) => (X, Y, Z) = (x, y, z);

    // 隱含 this 為 readonly,不可修改欄位
    public double Length => Math.Sqrt(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);
}

ref struct (C# 7.2+):僅限堆疊

// 僅能存在於堆疊,不可為類別欄位、陣列元素、封裝於物件
public ref struct SpanWrapper
{
    private readonly Span _span;
    
    public SpanWrapper(Span span) => _span = span;
    
    public int this[int index] => _span[index];
}

// 使用
Span stackSpan = stackalloc int[100];
var wrapper = new SpanWrapper(stackSpan); // 僅限區域變數、ref/out 參數

常見陷阱與最佳實務

陷阱後果解決方案
可變結構體複製語意導致 Bug、多執行緒競態一律設計為 不可變 (readonly 欄位、init 屬性)
大型結構體頻繁傳值複製開銷大於參考傳遞改 class 或 ref/out/in 參數
結構體實作介面並頻繁轉型裝箱配置、GC 壓力改類別、或泛型約束 where T : struct, IInterface
結構體預設值非預期 (全零)如 Guid.Empty、DateTime.MinValue 非業務有效值提供靜態工廠 Create()、或使用 Nullable<T>
結構體無參建構函式被隱含呼叫陣列配置、預設值初始化繞過自訂邏輯接受此事實,設計相容零值狀態;C# 10+ 可定義 struct() 但仍不保證被呼叫

標準庫值型別參考

  • System.Int32, Double, Boolean... (基本型別)
  • DateTime, DateTimeOffset, TimeSpan
  • Guid, Decimal
  • KeyValuePair<TKey, TValue>
  • Nullable<T> (值型別包裝器)
  • Span<T>, Memory<T> (ref struct)
  • ValueTuple<T1, T2...> (元組)
  • System.Numerics.Vector<T> (SIMD)

總結

結構體與類別的選擇本質是值語意 vs 參考語意的決策。小型、不可變、值語意、高頻配置選結構體 (如座標、錢幣、日期);複雜行為、多型、身分識別、可為 null、遞迴結構選類別。掌握 readonly struct、ref struct、in/ref/out 參數、裝箱成本等進階機制,並避開可變結構體、大型結構體複製等反模式,即能在效能與正確性間取得最佳平衡。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved