博客 / 詳情

返回

C#.NET ref struct 深度解析:語義、限制與最佳實踐

簡介

ref structC# 7.2 引入的一種特殊結構體類型,
它與普通 struct 的最大區別是 嚴格限制其分配位置:

ref struct 只能分配在棧(stack)上,不能分配在堆(heap)上。

⚡ 設計初衷

  • 提高性能:棧分配比堆分配快,並且無需 GC 回收。
  • 提供安全的內存訪問:保證生命週期受控,防止內存泄漏和懸空引用。
  • 適用於需要直接操作內存的場景,例如 Span<T>ReadOnlySpan<T>

關鍵特性

  • 只能分配在棧上,不能分配在堆上
  • 不能作為類的字段
  • 不能實現接口
  • 不能裝箱
  • 不能作為異步方法或迭代器的局部變量

基本語法

public ref struct MyStruct
{
    public int X;
    public int Y;

    public void Print() => Console.WriteLine($"{X}, {Y}");
}

與普通 struct 的區別

特性 struct ref struct
分配位置 棧或堆(例如在類中或裝箱時) 只能棧分配
裝箱(boxing) 支持(可轉為 object ❌ 禁止
接口實現 支持 ❌ 禁止(不能實現接口)
異步方法/迭代器 支持 ❌ 不能被 async/yield 捕獲
閉包捕獲 支持 ❌ 禁止
泛型約束 可作為泛型參數 ❌ 禁止用作類泛型參數
生命週期 受 GC 管理 完全受棧作用域約束

ref struct 的限制確保它 不會被錯誤地提升到堆中,保證其生命週期安全。

使用場景

ref struct 非常適合以下 高性能、低開銷 的場景:

場景 示例
內存切片 Span<T>ReadOnlySpan<T>
避免 GC 高頻分配和釋放的臨時數據結構
非託管資源訪問 指針操作、stackalloc 分配的緩衝區
網絡與數據解析 高性能序列化/反序列化(如 JSON、Protocol Buffers)

典型示例

Span<T>:最常見的 ref struct

Span<T> 是一個表示連續內存區域的類型:

Span<int> numbers = stackalloc int[5] { 1, 2, 3, 4, 5 };
numbers[2] = 99;

foreach (var n in numbers)
    Console.Write($"{n} "); // 輸出: 1 2 99 4 5
  • stackalloc 在棧上分配內存。
  • Span<T> 只能存在於當前方法棧中,離開作用域自動回收。

自定義 ref struct

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

    public double Length => Math.Sqrt(X * X + Y * Y);
}

void Demo()
{
    var p = new Point { X = 3, Y = 4 };
    Console.WriteLine(p.Length); // 5
}

與 stackalloc 配合

public static Span<byte> CreateBuffer()
{
    Span<byte> buffer = stackalloc byte[1024]; // 棧上分配 1KB
    buffer[0] = 42;
    return buffer; // ❌ 錯誤:不能返回 ref struct
}

返回 Span<T> 會導致棧內存逃逸,因此編譯器會報錯。

編譯器施加的約束

ref struct 的安全限制主要有以下幾點:

不能裝箱

ref struct MyStruct { }
object o = new MyStruct(); // ❌ 編譯錯誤

因為裝箱會將值類型複製到堆上。

不能實現接口

ref struct MyStruct : IDisposable { } // ❌ 編譯錯誤

接口調用可能導致提升到堆,破壞生命週期安全。

不能作為類字段

class MyClass
{
    public Span<int> SpanField; // ❌ 編譯錯誤
}

因為類實例在堆上,而 ref struct 只能存在棧上。

不能用作泛型參數

List<Span<int>> list = new(); // ❌ 編譯錯誤

不能捕獲到閉包

Span<int> span = stackalloc int[10];
Action action = () => Console.WriteLine(span[0]); // ❌ 編譯錯誤

閉包會將變量提升到堆中,破壞生命週期。

不能用於異步方法/迭代器

async Task Demo()
{
    Span<int> span = stackalloc int[10]; // ❌ 編譯錯誤
    await Task.Delay(1000);
}

異步狀態機會導致變量在堆上存儲。

與其他類型對比

特性 class struct ref struct
分配位置 棧/堆 僅棧
內存回收 GC 自動回收/GC 自動回收(方法退出時)
接口實現
裝箱/拆箱 ❌(本身是引用)
異步/閉包
典型代表 String DateTime Span<T>, ReadOnlySpan<T>

性能優勢

場景 普通 struct ref struct
分配/釋放速度 最快(僅棧操作)
GC 壓力 可能有(裝箱) 無 GC
內存局部性 較好 最佳
生命週期可控性 GC 管理 作用域結束即釋放

實戰示例:高性能字符串切片

public static int ParseDigits(ReadOnlySpan<char> span)
{
    int value = 0;
    foreach (var c in span)
    {
        if (!char.IsDigit(c)) break;
        value = value * 10 + (c - '0');
    }
    return value;
}

void Demo()
{
    string input = "12345abc";
    var slice = input.AsSpan(0, 5); // 直接操作原字符串內存
    Console.WriteLine(ParseDigits(slice)); // 輸出 12345
}

優勢:

  • 不會產生 Substring 帶來的額外堆分配。
  • 內存安全且性能接近指針操作。

總結

方面 説明
核心特性 只能分配在棧上,生命週期由作用域嚴格控制,無 GC 壓力
主要限制 不能裝箱、不能作為類字段、不能捕獲閉包、不能異步/迭代、不能實現接口
典型應用 Span<T>ReadOnlySpan<T>、高性能內存處理、網絡數據解析
最佳實踐 使用 using 範圍、readonly 修飾、避免逃逸、短生命週期
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.