Span 不能跨方法返回,因其为栈分配视图、生命周期绑定当前栈帧;应改用 Memory,它可跨方法、支持异步、可存字段;数组/字符串转 Span 零成本,Memory 包装数组亦无分配,但非托管内存需额外开销。

Span 不能跨方法返回,一返回就报 System.Span cannot be used in async methods
System.Span cannot be used in async methods这是因为 Span 是栈分配的“视图”,生命周期严格绑定当前栈帧。一旦方法返回,栈空间可能被复用,Span 就变成悬垂指针——CLR 直接禁止它逃逸到堆或异步上下文。
常见错误现象:CS8347 编译错误,或运行时报 System.ArgumentException: The Span。
- 正确做法:用
Memory替代——它是Span的“安全兄弟”,可跨方法、可 await、可存字段 - 如果只是临时切片(比如解析一段字节),就用
Span;如果要传给另一个方法处理、或作为返回值,一律用Memory -
Memory转Span很便宜:调用.Span属性即可,但只在当前同步上下文中安全
从数组、字符串、堆内存创建 Span/Memory 的写法差异
不同来源的底层存储模型不同,API 设计也不同,搞混容易踩空引用或越界。
- 数组 →
Span:new Span或(array) array.AsSpan()(推荐后者,更语义清晰) - 字符串 →
ReadOnlySpan:"hello".AsSpan(),注意是ReadOnlySpan,不能写入 - 堆上分配的 byte[](如
new byte[1024])→Memory:new Memory或(array) array.AsMemory() - 非托管内存(如
Marshal.AllocHGlobal)→ 必须用Memory+UnsafeMemoryManager或MemoryPool,不能直接套.Shared.Rent() Span
性能影响:数组和字符串转 Span 是零拷贝、零分配;而 Memory 包装数组时也不分配,但包装非托管内存时需额外元数据管理开销。
async 方法里想高效读取 Stream,别用 ReadAsync 返回 int,改用 ReadAsync(Memory, CancellationToken)
老式 Stream.ReadAsync(byte[], int, int) 会强制你传数组,容易触发 GC;新 API 支持直接传 Memory,配合 MemoryPool 可复用缓冲区。
- 典型写法:
var buffer = MemoryPool<byte>.Shared.Rent(4096); try { var read = await stream.ReadAsync(buffer.Memory, token); Process(read); // read 是实际读到的长度 } finally { buffer.Dispose(); // 必须调用,否则池泄漏 }</byte> - 容易漏掉的坑:
buffer.Memory是整个租来的空间,read才是有效长度,别直接拿buffer.Memory.Span全量处理 - 兼容性:.NET Core 2.1+ / .NET 5+ 支持;.NET Framework 不支持该重载,得降级或用第三方适配
Span 和 Memory 在 foreach 中不能直接用,会编译失败
foreach (var b in span) 看似合理,但 Span 没实现 IEnumerable,也没有公开的枚举器类型,编译器找不到 GetEnumerator。
- 正确遍历方式:用
for循环 +.Length,这是最高效且唯一可靠的方式for (int i = 0; i - 如果真想用类似 foreach 的语义,可以用
span.ToArray()(不推荐,分配堆内存)或封装成SpanSequence(自定义结构体 +GetEnumerator),但多数场景没必要 - 注意:
Memory同样不支持foreach,必须先转.Span再用for
这事看着小,但新手常卡在这儿反复试错,其实根本原因就一条:Span 是栈视图,设计上就拒绝“泛型枚举”这种需要堆分配迭代器的模式。










