readonlyspan是字符串切片唯一合理选择,因它是.net为string专设的栈上结构体,仅存指针和长度、零拷贝零gc;而substring必堆分配,memory无法隐式转换且包装即分配,span不适用只读场景。

ReadOnlySpan
为什么 ReadOnlySpan 是字符串切片的唯一合理选择
.NET 为 string 专门设计了 ReadOnlySpan<char></char> 这个栈上结构体,它只存底层字符数组指针和长度,不复制数据、不触发 GC。调用 "hello".AsSpan() 是纯视图构造,开销趋近于零。
-
string.Substring()每次都 new 一个新string,高频解析(如 HTTP header、CSV 行)会明显抬高 GC 压力 -
Memory<char></char>无法从string隐式转换,强行包装需先ToCharArray()→ 触发堆分配,违背初衷 -
Span<char></char>不适用于字符串(内容不可写),用ReadOnlySpan<char></char>更安全、兼容性更好
函数签名必须直接声明为 ReadOnlySpan
如果方法参数是 string,哪怕第一行就调 .AsSpan(),也已经晚了——参数传入时 CLR 就隐式执行了一次 ToString()(对 string 是恒等操作,但仍是堆对象引用传递,且编译器无法优化掉该语义)。
- 正确写法:
bool TryParseHeader(ReadOnlySpan<char> input, out string key, out string value)</char> - 调用时传
headerLine.AsSpan(),不是headerLine - 下游若只认
string,优先找 BCL 重载,如int.TryParse(ReadOnlySpan<char>, out int)</char>、string.Equals(ReadOnlySpan<char>, string, StringComparison)</char>
Slice(start, length) 容易踩的边界坑
Slice() 在 Release 模式下不做越界检查,start + length > span.Length 会直接抛 System.IndexOutOfRangeException,不是静默截断。
-
input.Slice(3, 5)表示“从索引 3 开始取 5 个字符”,不是[3..5);C# 8+ 的范围语法input[3..8]更直观,语义等价 - 若
start或length来自外部(如解析Content-Range头),务必提前校验:var safeLen = Math.Min(length, input.Length - start) - 别依赖
try/catch捕获越界异常——性能开销远高于一次Math.Min() - 判断长度用
input.Length,不要用input.ToString().Length,后者又分配又慢
哪些地方绝对不能用 ReadOnlySpan
编译器会拦住大部分逃逸行为,但有些坑得靠经验避开:
- 不能作为类字段、
static变量、async方法参数或 lambda 捕获变量——ref struct类型会被编译器直接报错 - 不能从
StringBuilder.ToString()的结果创建:sb.ToString().AsSpan()中的ToString()已分配,再包 Span 无意义 - 不能在循环里反复调
.ToString():哪怕只在最后一步转,也应确保只调一次,否则“用了 Span 却白忙活” - 若需长期持有某段文本(如缓存),该用原始
string或ReadOnlyMemory<char></char>+ 自定义IMemoryOwner<char></char>,而非硬套Memory<char></char>
真正难的是生命周期控制——只要原 string 在 Span 使用期间没被 GC 掉,它就安全;一旦涉及局部 StringBuilder、异步上下文或跨帧传递,就得立刻换回传统方式或重构数据流。










