簡介
ref struct 是 C# 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 修飾、避免逃逸、短生命週期 |