一句话概括
Span<T> 是一段连续内存的"窗口",只记录起点 和长度,不复制数据。它让你用安全的方式高效地处理数组、字符串和其他内存,尽量避免创建新对象。
为什么需要它
传统写法中,取一段数据通常要复制一份,比如 Substring、Split、Array.Copy,每次都会创建新对象。这些对象用完后要由垃圾回收器(GC)清理。在高并发的程序里,大量临时对象会让 GC 频繁工作,导致程序变慢。
Span 的思路就像看书时夹书签:不复印书页,只告诉别人"从第几页开始看,看几页"。
基本用法
csharp
int[] numbers = { 10, 20, 30, 40, 50 };
Span<int> part = numbers.AsSpan(1, 3); // 看第 1 个开始的 3 个元素:20, 30, 40
Span<int> tail = numbers.AsSpan()[2..]; // 范围语法:30, 40, 50
part[0] = 999; // 修改的是原数组
Console.WriteLine(numbers[1]); // 999
处理字符串时使用只读版本 ReadOnlySpan<char>:
csharp
string date = "2026-10-27";
int year1 = int.Parse(date.Substring(0, 4)); // 创建了新字符串
int year2 = int.Parse(date.AsSpan(0, 4)); // 不创建新字符串
可以指向哪些内存
Span 统一了多种内存来源,一个接受 Span 的方法可以处理所有这些情况:
csharp
Span<int> a = new int[10]; // 普通数组
Span<byte> b = stackalloc byte[64]; // 栈上内存,无需 unsafe
ReadOnlySpan<char> c = "hello".AsSpan(); // 字符串
Span<int> d = [1, 2, 3]; // 集合表达式(C# 12+)
三个相关类型
Span<T> 可读可写,只能在方法内部临时使用。ReadOnlySpan<T> 是只读版本,字符串、常量数据都用它。Memory<T>(以及 ReadOnlyMemory<T>)可以理解为"能长期保存的 Span",可以放进类的字段、可以跨越 await,需要读写时通过 .Span 取出一个 Span 来用。
简单的选择原则是:同步方法用 Span,异步方法或需要保存时用 Memory。
使用限制及原因
Span 是一种特殊的结构体(ref struct),只能存在于栈上。因此它不能作为类的字段,不能被装箱成 object,不能被 lambda 捕获,也不能跨越 await(C# 13 起允许在 async 方法里使用,只要不跨 await)。
原因很直接:Span 可能指向栈上的临时内存,方法结束后这块内存就失效了。如果允许 Span 被存到堆上的对象里,就可能访问到已经失效的内存。这些限制由编译器在编译时检查,从根本上避免了这类错误。
为什么快
除了避免分配,Span 还有底层优化。它内部持有的是 GC 能追踪的托管引用,不需要用 fixed 钉住对象;JIT 编译器在标准 for 循环中能去掉边界检查;IndexOf、SequenceEqual 等方法使用了 SIMD 指令,一次处理多个元素。
常用配套 API
int.Parse、TryFormat 等方法都有 Span 重载,可以直接解析或格式化而不产生中间字符串。BinaryPrimitives 用于按字节序读写二进制数据。ArrayPool<T> 用于租借和归还大缓冲区。string.Create 可以直接写入新字符串的内部缓冲区。MemoryExtensions.Split(.NET 8+)可以零分配地拆分字符串。
实用建议
stackalloc 只用于小块内存,大块内存改用 ArrayPool 租借,常见写法是"小于 256 用栈,否则租数组",并在 finally 里归还。避免在性能关键代码中调用 ToArray() 或 ToString(),否则就失去了 Span 的意义。比较内容时使用 SequenceEqual,因为 == 比较的是是否指向同一块内存。设计 API 时优先让参数接受 ReadOnlySpan<T>,这样调用方传数组、字符串或切片都可以。
版本演进
Span 从 .NET Core 2.1(C# 7.2)开始引入,之后语言不断增强对它的支持:C# 11 加入 ref 字段和 scoped,C# 12 支持集合表达式,C# 13 支持 params ReadOnlySpan<T> 和泛型的 allows ref struct,C# 14 让数组、字符串到 Span 的转换成为语言内建的隐式转换,使用起来更加自然。
结语
不必在每行代码中都追求零分配。日常开发中,记住"Span 是不复制数据的窗口",在字符串解析、二进制处理、高频调用的代码里优先考虑它,就能以很小的代价获得明显的性能提升。