Span仅在需切片、避免复制、生命周期短三者同时成立时才适用;Substring因返回新string必堆分配,AsSpan().Slice则零分配仅存地址与长度。

Span 不是万能加速器,用错地方反而更慢;它只在“需要切片、避免复制、生命周期短”这三者同时成立时才值得用。
为什么 string.Substring 一调就分配,而 AsSpan().Slice 不分配
因为 Substring 必须返回一个新 string 对象,而 string 是不可变引用类型,底层要拷贝字符数据到堆上;AsSpan() 返回的是 ReadOnlySpan<char></char>——纯栈上结构体,只存起始地址和长度,不碰原字符串内存内容。
- 调用
str.Substring(10, 5)→ 分配约 10 字节堆内存(UTF-16) - 调用
str.AsSpan().Slice(10, 5)→ 零分配,生成一个 16 字节结构体(x64) - 但注意:
Slice(10, 5)的第二个参数是长度,不是结束索引;越界会直接抛System.IndexOutOfRangeException,不会静默截断 - 如果后续必须传给只认
string的老 API,.ToString()这一刻才分配——别提前转,尤其别在循环里反复转
Span 不能当返回值或字段,该用 Memory 的时候别硬扛
Span<t></t> 是 ref struct,编译器禁止它逃逸出当前栈帧。一旦你写 return span; 或 public Span<byte> Buffer;</byte>,立刻报 CS8347 或运行时报 The Span<t> is not stack-only</t>。
- 跨方法传递切片:改用
Memory<t></t>,它可存字段、可 await、可序列化(需额外包装) - 从数组创建:优先用
array.AsMemory(),比new Memory<t>(array)</t>更语义清晰 -
Memory<t></t>转Span<t></t>很便宜:memory.Span属性即可,但只在同步上下文中安全 - 异步读流场景:必须用
stream.ReadAsync(Memory<byte>, CancellationToken)</byte>,配合MemoryPool<byte>.Shared.Rent()</byte>复用缓冲区
stackalloc 分配栈内存,快是快,但容易栈溢出
stackalloc 在当前方法栈帧上直接划一块内存,不走 GC,访问极快。但它不是“无限大内存”,超出线程默认栈大小(通常 1MB)就会触发不可恢复的 StackOverflowException,连 try/catch 都捕获不到。
- 安全阈值建议:单次
stackalloc不超过 8KB(如stackalloc byte[8192]),小数据解析够用 - 别在递归函数或深层调用链中用
stackalloc - 启用
unsafe上下文是前提,项目文件需设<LangVersion>7.2</LangVersion>或更高 - 用完无需手动释放,函数返回即自动回收——这是它和堆分配最根本的区别
ReadOnlySpan 入参是零分配字符串解析的关键入口
函数签名决定一切。哪怕内部第一行就写 input.AsSpan(),只要参数类型是 string,调用方传入那一刻就已经触发一次堆分配了。
- 正确入口写法:
bool TryParse(ReadOnlySpan<char> input, out int value)</char> - 调用时必须传
data.AsSpan(),不是data - 内置 API 已大量支持:
int.TryParse(ReadOnlySpan<char>, out int)</char>、Utf8Parser.TryParse、Span<char>.IndexOf</char>等 - 别试图把
ReadOnlySpan<char></char>存进字典或缓存——它不保证底层字符串长期存活,GC 可能随时回收原字符串
Span 的真正难点不在语法,而在对数据生命周期的判断:你得清楚知道那块内存是谁分配的、何时释放、是否跨线程——稍有不慎,要么越界崩溃,要么悬垂引用。它把一部分 C++ 级别的责任,悄悄交还给了开发者。



















