Span 和 Memory 是 C# 7.2 引入的核心高性能内存抽象类型,专为避免堆分配、减少 GC 压力、提升数据处理效率而设计。它们不是“替代数组”,而是更安全、更灵活的“内存视图”——能指向栈内存、堆数组、本机内存甚至只读数据,且全程不触发装箱或额外拷贝。
Span:栈友好的零分配切片工具
Span 是 ref-like 类型,只能在栈上声明(如局部变量、方法参数),不能作为字段、不能被装箱、不能跨 await 边界。它的价值在于对已有内存做“零成本切片”和“就地操作”。
从数组创建:Span span = array.AsSpan(); —— 不复制,只是引用起始地址+长度
切片操作:span.Slice(2, 5) —— O(1) 时间,返回新 Span,原数据不动
配合 stackalloc:Span buffer = stackalloc byte[256]; —— 栈上分配,无 GC 开销
写入字符串字节(UTF8):Utf8Encoder.Encode(buffer, "hello", out int written);
Memory:可跨作用域的托管内存视图
Memory 是 Span 的“托管友好版”,可作为字段、参数、返回值,甚至用于 async 方法。它背后封装了 IMemoryOwner 或数组等资源,但本身不拥有内存。
从数组创建:Memory mem = array; 或 array.AsMemory()
获取可写 Span:Span span = mem.Span; —— 安全转为栈视图操作
异步场景示例:async Task ProcessAsync(Memory data) —— 调用方传入,方法内用 .Span 处理
注意:Memory 本身不可序列化,也不直接暴露指针,安全性高于裸指针
常见高性能组合模式
真实项目中,Span/Memory 往往和特定 API 协同使用,才能释放最大效能。
字符串解析(不用 Split/ToArray):用 ReadOnlySpan 遍历、IndexOf、Slice,跳过所有字符串分配
二进制协议处理:Socket.ReceiveAsync 返回 Memory,直接用 BinaryPrimitives 读取 int/long,避免 ArraySegment 封装开销
池化 + Memory:搭配 ArrayPool.Shared.Rent() 获取数组 → 转为 Memory → 处理完 Return(),复用缓冲区
只读场景优先用 ReadOnlySpan/ReadOnlyMemory:编译器会阻止意外写入,语义清晰,性能一致
必须避开的坑
Span 和 Memory 强大,但违反规则会导致编译失败或运行时异常(如 System.SpanHelpers.ThrowInvalidOperationIfNotInitialized)。
Span 不能存为类字段 —— 编译器直接报错:“Cannot declare a variable of type 'Span' in a context where it cannot be used”
不能在 async 方法中 await 后继续用之前捕获的 Span —— 因为可能跨栈帧,生命周期失控;应改用 Memory
stackalloc 分配过大(如 >1MB)可能栈溢出 —— 生产环境慎用超大 stackalloc,优先考虑 ArrayPool
不要把 Span 暴露给不受信代码 —— 它可绕过部分数组边界检查(虽仍受 CLR 内存保护)
基本上就这些。Span 和 Memory 不是炫技工具,而是当你真在处理高频日志解析、网络包拆包、图像像素遍历、JSON 流式反序列化等场景时,最值得投入理解的底层利器。用对了,性能提升常是 2–5 倍,GC 次数直线下降。