ReadOnlySpan适用于短时、只读、高频切片解析场景;错误使用会引发堆分配或崩溃,正确用法包括直接AsSpan()、stackalloc创建、流读取后转Span,且须严守生命周期与长度安全。

ReadOnlySpan<char></char> 不是“更快的 string”,它是完全不同的东西:零分配、栈驻留、生命周期受限。用错地方,性能反而更差;用对场景,能直接砍掉 Gen0 GC。
什么时候该用 ReadOnlySpan<char></char> 而不是 string
核心判断就一条:你是否只做「短时、只读、高频」的切片解析?
- HTTP header 行里找
:并提取值:line.IndexOf(':') + line.Slice() - CSV 行按
,拆字段,每个字段只用来int.TryParse()或字面量比对 - 日志行中提取时间戳、状态码、路径段——不存、不拼接、不传给老框架(如 ASP.NET MVC 的
ViewBag)
反例:
- 要塞进
ViewBag、要序列化成 JSON、要缓存 5 秒以上 - 要跨
await传递——编译器会直接报错:ref struct不能出现在异步方法签名中 - 要作为类字段存储——编译失败,
ReadOnlySpan<t></t>是ref struct
怎么安全创建 ReadOnlySpan<char></char>
源头决定一切。以下写法看似合理,实则已触发堆分配,性能归零:
-
new ReadOnlySpan<char>(someString.ToCharArray())</char>—— 数组分配 + 复制,双重浪费 -
someString.Substring(10, 5).AsSpan()——Substring先建新string,再转Span已晚 -
someString.AsSpan().ToString()在函数开头就调 —— 前面所有优化白做
正确做法只有三种:
- 直接从
string创建:someString.AsSpan()(安全,无额外分配) - 从栈数组创建:
stackalloc char[256]后用new ReadOnlySpan<char>(ptr, len)</char> - 从
byte[]或文件流读取后创建:stream.Read(buffer); var span = buffer.AsSpan(0, bytesRead)
Slice(start, length) 和范围语法 [start..end] 的坑
Slice(start, length) 容易被当成 [start..end) 用,但它真义是「从 start 开始取 length 个字符」。越界在 Release 模式下不抛异常,直接崩。
- 输入
"abcde",span.Slice(3, 5)→ 从索引 3 开始取 5 个 → 实际需要长度 8 →IndexOutOfRangeException - 外部数据(如解析位置)必须手动截断:
var safeLen = Math.Min(requestedLen, input.Length - start),别依赖try/catch - C# 8+ 推荐用范围语法:
input[3..8]语义清晰,底层仍是Slice,但编译器帮你算长度 -
input[3..]和input[..5]是合法的,但input[^3..](倒序索引)需注意^运算本身有开销,高频热路径慎用
传参和返回值的生命周期红线
ReadOnlySpan<char></char> 绑定原始字符串生命周期,不能返回给调用方长期持有。
- 函数签名必须声明为
ReadOnlySpan<char></char>,不能是string—— 否则一进方法就隐式调.ToString() - 已支持
ReadOnlySpan<char></char>的 BCL 方法要优先用,比如:int.TryParse(ReadOnlySpan<char>, out int)</char>、string.Equals(ReadOnlySpan<char>, string, StringComparison)</char> - 切完的
ReadOnlySpan<char></char>仍绑定原始字符串生命周期,不能返回给调用方长期持有 - 最大陷阱是
.ToString():写了void ParseLogLine(ReadOnlySpan<char> line)</char>,结果第一句就是line.ToString()—— 这行代码立刻让前面所有优化失效
真正难的不是写对语法,而是守住「不跨作用域」「不碰 .ToString()」「不误算 length」这三条线。一旦松动,零分配优势瞬间归零。


















