span是.net中处理字符串和字节数组的零分配安全视图,需严格匹配内存生命周期:.asspan()安全高效,tostring().asspan()危险易致悬垂引用,异步场景应改用memory。

SpanSubstring、ToArray、Array.Copy,就该立刻换成 Span。
怎么从 string / byte[] 创建 Span 而不踩坑
创建方式决定是否真正零分配,也决定能否安全使用:
-
"hello".AsSpan()是安全的,返回ReadOnlySpan<char></char>,底层直接指向字符串内存,无拷贝;但不能写入(字符串不可变) -
new byte[1024].AsSpan()安全,返回可写Span<byte></byte>,所有修改直接作用于原数组 -
StringBuilder.ToString().AsSpan()❌ 危险:每次ToString()都 new 一个新字符串,AsSpan()指向的是临时对象,可能被 GC 提前回收;应改用builder.AsSpan()(需 .NET 6+) -
stackalloc byte[256]可用于栈上零 GC 分配,但长度必须严格校验(如超过 1MB 易触发StackOverflowException)
为什么 Slice(start, length) 比 Substring 快,又容易误用
Slice 本质只是偏移 + 长度重封装,O(1),不复制数据;但它是值类型,频繁调用会生成大量临时结构体:
- 高频循环中写
span.Slice(i, 1)做单字节遍历?没问题,开销极小 - 但若在深嵌套方法中反复
span.Slice(0, n).Slice(1, m).Slice(...),虽仍零分配,却增加栈帧体积和 CPU 指令数 -
Substring的错误不是“慢”,而是每次分配新string,触发 GC —— 在 HTTP header 解析这种每秒万次的路径里,它就是瓶颈源头 - 正确替代:
ReadOnlySpan<char> part = input.AsSpan().Slice(start, length);</char>
Span 不能跨 await / 不能当字段,那异步或长期持有怎么办
编译器对 Span<t></t> 的限制(CS8345、CS8361)不是 bug,是强制你做正确的事:
-
async Task ProcessAsync(Span<byte> data)</byte>❌ 编译失败:await 后无法保证栈帧存活 - 正确写法:
async Task ProcessAsync(Memory<byte> data)</byte>,进方法后立刻取data.Span操作 - 要缓存缓冲区?用
private Memory<byte> _buffer;</byte>,而不是Span<byte></byte>字段 - 从池里租内存:
var array = ArrayPool<byte>.Shared.Rent(4096); var mem = array.AsMemory();</byte>,处理完记得Return
ReadOnlySpan 和 Span 到底该用哪个
绝大多数字符串解析场景,你应该默认用 ReadOnlySpan<char></char>:
-
int.TryParse(ReadOnlySpan<char>, out int)</char>、Span<char>.IndexOfAny</char>等 API 都优先支持只读版本 - 编译器会阻止意外写入,语义更清晰,且性能与可写版完全一致
- 只有当你明确需要修改原始字符(比如 in-place 大小写转换、填充占位符),才用
Span<char></char>,且前提是源数据本身可写(如堆数组) - 别试图用
Span<char></char>修改字符串内容——string是不可变的,这么做要么编译不过,要么运行时报错
Span 的复杂点不在语法,而在生命周期边界感:它要求你时刻意识到“这段内存此刻归谁管、能活多久”。一旦习惯把 Span 当作“临时视图”而非“数据容器”,那些编译错误就不再是阻碍,而是提醒你重构设计的信号。










