readonlyspan 替代 string 和 substring 是 .net 中开销最低的字符串切片优化方式,因其零分配、零拷贝、无 gc;但需严控生命周期、避免隐式转 string,否则性能归零。

直接用 ReadOnlySpan<char></char> 替代 string 参数和中间 Substring,是目前 .NET 中最有效、开销最低的字符串切片优化方式——但必须严格控制生命周期、避免隐式转 string,否则性能归零。
为什么 ReadOnlySpan 比 Substring 快得多
关键不在“快”,而在“不分配”:Substring 每次都新建 string 对象,堆上多出几 MB 分配;ReadOnlySpan<char></char> 只存起始地址 + 长度,零拷贝、零 GC。10 万次日志行字段提取,用 Substring 可能触发多次 Gen0 GC,而用 ReadOnlySpan<char></char> 基本无压力。
- 适用场景:HTTP header 解析、CSV 行拆分、协议命令字提取(短时、只读、高频)
- 不适用场景:需要序列化、传给老框架(如 ASP.NET MVC 的
ViewBag)、或要长期缓存结果 - 注意:.NET Core 2.1+ 才原生支持,.NET Framework 不可用
如何安全创建和传递 ReadOnlySpan
源头决定安全性。常见错误是“看着像 Span,实则已掉回堆分配”:
- ✅ 正确:
"key=value".AsSpan()、stackalloc char[128]后转ReadOnlySpan、array.AsSpan() - ❌ 危险:
StringBuilder.ToString().AsSpan()——ToString()已分配新字符串,再AsSpan()也晚了 - ❌ 错误:
new ReadOnlySpan<char>(someString.ToCharArray())</char>—— 数组分配 + 复制,双重浪费 - 传参时函数签名必须声明为
ReadOnlySpan<char></char>,不能是string,否则一进方法就隐式调.ToString()
Slice 和范围语法的坑:length 不是 end 索引
Slice(start, length) 容易被当成 [start..end) 用,但它是“从 start 开始取 length 个字符”。越界不会在 Release 模式下检查,直接抛 System.IndexOutOfRangeException。
-
input.Slice(3, 5)→ 从索引 3 起,取 5 个字符(即索引 3–7) -
input[3..8](C# 8+)→ 等价,语义更清晰,底层仍是Slice - 若
start或length来自外部(如解析位置),务必先做截断:var safeLen = Math.Min(length, input.Length - start),别依赖try/catch - 切完的
ReadOnlySpan<char></char>仍绑定原始字符串生命周期,不能返回给调用方长期持有
常见性能归零操作:ToString() 是最大陷阱
写了 void ParseLogLine(ReadOnlySpan<char> line)</char>,结果里面第一句就是 line.ToString() —— 这行代码立刻让前面所有优化失效,堆上又多一个字符串对象。
- 优先用原生方法:比如
line.IndexOf(':')、line.Trim()、int.TryParse(line, out int val) - 只有真正需要字符串(如日志输出、异常消息、JSON 序列化)时,才调
.ToString(),且尽量只调一次 - 不要在循环里反复
.ToString(),尤其不要写成for (int i = 0; i - 第三方库不支持
ReadOnlySpan<char></char>?先确认是否真不可绕过;否则可临时用stackalloc char[256]+Encoding.UTF8.GetBytes()构造字节视图
最复杂的地方从来不是怎么写 Slice,而是判断“此刻是否真的需要一个 string”——多数时候,你不需要。











