span 不能跨 await 边界使用,因其为栈分配的 ref 类型,而 await 会将局部变量提升至堆上状态机中,导致编译错误 cs8346;正确做法是异步用 memory,同步处理时再转 span。

Span 为什么不能跨 await 边界使用
因为 Span<t></t> 是栈分配的 ref 类型,编译器禁止它逃逸到堆上——而 await 会触发状态机生成,可能把局部变量提升到堆上的状态机结构体里。一旦你写 async Task<span>> ReadAsync()</span>,编译器直接报错:CS8346: Cannot use a variable of type 'Span<byte>' in an async method</byte>。
常见错误现象:在异步读取网络流或文件后想直接返回 Span<byte></byte>,结果编译不过,或强行用 Memory<t></t> 却没意识到后续切片仍受限。
- 正确做法:异步操作用
Memory<t></t>接收(如await stream.ReadAsync(buffer)中的buffer是Memory<byte></byte>) - 同步处理阶段再转成
Span<byte></byte>:比如var span = memory.Span,只在方法内做解析、查找、拷贝等短生命周期操作 - 别试图把
Span<t></t>存进字段、属性或返回给调用方——它不是设计来“传递”的,而是用来“临时操作”的
Memory 和 Span 的参数传递差异
二者语义不同:Span<t></t> 表示「当前栈上下文里一块连续内存」,Memory<t></t> 表示「一段可安全跨上下文访问的内存视图」。这个区别直接决定你该在哪儿用哪个。
使用场景举例:写一个通用字符串解析函数,输入可能是数组、堆内存、本机内存、甚至 unmanaged 指针。
- 如果函数只做纯内存扫描(比如找第一个逗号),用
ReadOnlySpan<char></char>参数最轻量、零分配、无 GC 压力 - 如果函数需要把结果缓存起来供后续多次访问,或要传给另一个异步方法,必须用
ReadOnlyMemory<char></char>—— 它能封装ArraySegment<t></t>、T[]、string等多种来源 - 注意:从
Memory<t></t>转Span<t></t>是廉价的(只是字段复制),但反过来不行;Span<t></t>无法隐式转成Memory<t></t>
用 AsSpan() 切片时最容易漏掉的边界检查
AsSpan() 本身不校验索引合法性,越界不会抛异常,而是引发未定义行为(尤其在 Release 模式下可能静默读错数据或崩溃)。
常见错误现象:解析协议头时写 data.AsSpan(4, 2),但 data.Length 实际是 5,结果拿到一个长度为 2 的 Span,却只覆盖了最后 1 字节有效数据——后续逻辑全错,且很难复现。
- 永远在切片前手动检查:
if (offset + length > source.Length) throw new ArgumentException(...) - 优先用带起止索引的重载:
source.AsSpan(start, length)比source.AsSpan().Slice(start, length)更直观,也更容易加 guard - 第三方库(如
System.IO.Pipelines)返回的ReadOnlySequence<t></t>不支持直接AsSpan(),得先.FirstSpan或.ToArray(),这里容易误判长度
性能陷阱:ToString() 和 string.Concat 对 Span 的隐式转换
Span<char></char> 不能直接传给 Console.WriteLine() 或拼接进 string,C# 会自动调用 ToString(),而这会触发一次堆分配——瞬间把所有优化抵消掉。
典型反模式:var span = "hello".AsSpan(); Console.WriteLine(span); 表面看没 new,实际背后调了 span.ToString() → new string → GC 压力。
- 打印调试时,显式转
string并接受开销:Console.WriteLine(span.ToString()),但生产代码中避免 - 拼接多个
Span<char></char>?别用string.Concat,改用string.Create+Span<char></char>回调,或者用StringBuilder配合AppendSpan(.NET 6+) - 序列化场景(如 JSON 输出):优先选支持
Span<byte></byte>写入的库(如System.Text.Json的Utf8JsonWriter),别先转string再编码
真正难的不是学会怎么写 Span<t></t>,而是每次伸手去拿它的时候,都得下意识问一句:这段内存此刻归谁管?它会不会活过当前栈帧?有没有人正在偷偷把它变成 string?











