很多开发者第一次接触 ref struct,往往先记住一串“不能做什么”的规则:不能装箱、不能实现接口、不能进 async、不能放进类字段。可如果不理解这些限制为什么存在,实际写代码时还是很容易踩坑。
这篇文章把 ref struct 放回它的设计背景里来讲:先看它和普通 struct 到底差在哪,再看编译器为什么要强行限制使用方式,最后结合 Span、stackalloc 和字符串切片示例,判断它究竟适不适合你的场景。
什么是 ref struct,为什么 C# 7.2 要引入它
在 C# 7.2 中,ref struct 是一种特殊的结构体类型。它和普通 struct 最大的不同,不是语法,而是分配位置和生命周期约束:
ref struct 只能分配在栈(stack)上,不能分配在堆(heap)上。
这个设计主要服务于三件事:
- 提高性能:栈分配通常比堆分配更快,而且不需要等待
GC回收。 - 保证内存安全:生命周期被限定在当前作用域内,能减少悬空引用和错误逃逸。
- 支持底层内存操作:像
Span、ReadOnlySpan这类需要直接表示一段连续内存的类型,就依赖这类语义。
因此,ref struct 不是“更快的 struct”这么简单,它本质上是 C# 为受控的栈上内存视图准备的一套语言约束。
基本语法与普通 struct 的核心区别
基本语法
public ref struct MyStruct
{
public int X;
public int Y;
public void Print() => Console.WriteLine($"{X}, {Y}");
}
从声明方式看,它只是在 struct 前多了一个 ref。但这个关键字带来的,不是“按引用传递”,而是编译器对类型使用边界的整套限制。

与普通 struct 的区别
| 特性 | struct | ref struct |
|---|---|---|
| 分配位置 | 栈或堆(例如在类中或装箱时) | 只能栈分配 |
| 装箱(boxing) | 支持(可转为 object) | 禁止 |
| 接口实现 | 支持 | 禁止(不能实现接口) |
| 异步方法/迭代器 | 支持 | 不能被 async/yield 捕获 |
| 闭包捕获 | 支持 | 禁止 |
| 泛型约束 | 可作为泛型参数 | 禁止用作类泛型参数 |
| 生命周期 | 受 GC 管理 | 完全受栈作用域约束 |
可以把它理解成:普通 struct 仍然有机会因为装箱、字段嵌套或泛型容器而“上堆”,而 ref struct 的全部规则,都是为了防止这种情况发生。
为什么它限制这么多
ref struct 的各种限制看起来零散,实际都指向同一个目标:禁止实例逃逸到堆上。一旦逃逸,栈上数据的生命周期就可能已经结束,类型安全也随之失效。

不能装箱
ref struct MyStruct { }
object o = new MyStruct(); // 编译错误
装箱会把值类型复制到堆上,因此编译器直接禁止。
不能实现接口
ref struct MyStruct : IDisposable { } // 编译错误
接口调用链可能间接导致对象被当作堆对象处理,这会破坏 ref struct 的生命周期模型。
不能作为类字段
class MyClass
{
public Span SpanField; // 编译错误
}
类实例本身位于堆上,因此类字段如果是 ref struct,就等于把它间接放到了堆里。
不能用作泛型参数
List> list = new(); // 编译错误
很多泛型容器天然就以堆分配为前提,例如 List。这与 ref struct 的存在条件冲突。
不能被闭包捕获
Span span = stackalloc int[10];
Action action = () => Console.WriteLine(span[0]); // 编译错误
闭包会把局部变量提升到堆中保存,Span 一旦被捕获,生命周期就不再受当前栈帧控制。
不能用于异步方法或迭代器
async Task Demo()
{
Span span = stackalloc int[10]; // 编译错误
await Task.Delay(1000);
}
原因和闭包类似:async/yield 会把方法改写成状态机,而状态机对象通常存储在堆上。
所以这类限制不是语言“故意难用”,而是在编译阶段阻断所有可能让栈数据越界生存的路径。
哪些场景适合使用 ref struct
ref struct 适合那些对性能敏感、生命周期短、并且明确围绕“当前调用栈内处理数据”的场景。
| 场景 | 示例 |
|---|---|
| 内存切片 | Span、ReadOnlySpan |
| 避免 GC | 高频分配和释放的临时数据结构 |
| 非托管资源访问 | 指针操作、stackalloc 分配的缓冲区 |
| 网络与数据解析 | 高性能序列化/反序列化(如 JSON、Protocol Buffers) |
反过来说,如果一个类型需要跨方法长期保存、放进集合、参与异步流程,或者要通过接口统一抽象,那么它通常就不适合设计成 ref struct。
典型示例:Span、自定义类型与 stackalloc
Span:最常见的 ref struct
Span 用来表示一段连续内存区域,是 ref struct 最常见也最实用的代表。
Span 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只能存在于当前方法栈中,离开作用域自动回收。
自定义 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
}
这说明 ref struct 并不只服务于框架内置类型。只要你的类型确实需要严格依附当前作用域,也可以自定义。
与 stackalloc 配合时要注意什么
public static Span CreateBuffer()
{
Span buffer = stackalloc byte[1024]; // 栈上分配 1KB
buffer[0] = 42;
return buffer; // 错误:不能返回 ref struct
}
这里的关键问题是“栈内存逃逸”。buffer 指向当前方法栈上的 1KB 空间,方法返回后,这块空间已经失效,因此编译器会直接报错。
和 class、struct 放在一起看更容易理解
| 特性 | class | struct | ref struct |
|---|---|---|---|
| 分配位置 | 堆 | 栈/堆 | 仅栈 |
| 内存回收 | GC | 自动回收/GC | 自动回收(方法退出时) |
| 接口实现 | |||
| 装箱/拆箱 | (本身是引用) | ||
| 异步/闭包 | |||
| 典型代表 | String | DateTime | Span, ReadOnlySpan |
如果只看“值类型”这个标签,struct 和 ref struct 很像;但只要进入装箱、泛型容器、异步状态机这些真实工程场景,两者的行为边界就完全不同了。
性能优势在哪里,以及一个实际解析示例
性能优势
| 场景 | 普通 struct | ref struct |
|---|---|---|
| 分配/释放速度 | 快 | 最快(仅栈操作) |
| GC 压力 | 可能有(装箱) | 无 GC |
| 内存局部性 | 较好 | 最佳 |
| 生命周期可控性 | GC 管理 | 作用域结束即释放 |
这里要注意,ref struct 的性能价值不是“所有场景都更快”,而是在短生命周期、频繁处理内存片段时,能绕开额外分配和 GC 成本。

实战示例:高性能字符串切片
public static int ParseDigits(ReadOnlySpan 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
}
这个例子里,ReadOnlySpan 的优势很直接:
- 不会产生
Substring带来的额外堆分配。 - 内存安全,同时性能接近底层指针访问。
这也是现代 .NET 高性能文本处理大量采用 Span/ReadOnlySpan 的原因。
使用建议:什么时候该用,什么时候别硬上
从工程实践看,ref struct 最适合做“局部、高性能、短生命周期”的工具型类型,而不是通用业务模型。
- 优先用于内存切片、缓冲区处理、解析器这类热点路径。
- 避免让它承担集合存储、接口抽象、跨线程异步流转等职责。
- 设计时优先考虑是否存在“逃逸到方法外”的需求,只要有,通常就该换方案。
- 在 API 设计上,尽量保持作用域清晰、生命周期短,避免把代码写成隐式依赖栈上下文的复杂结构。
一句话概括:ref struct 的价值建立在限制之上。你接受这些限制,才能换来性能和安全;如果业务本身需要灵活流转,那普通 struct 或 class 往往更合适。







