.NET 10之Span技术总结

一句话概括

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 是不复制数据的窗口",在字符串解析、二进制处理、高频调用的代码里优先考虑它,就能以很小的代价获得明显的性能提升。

相关推荐
1-1=02 小时前
.NET Core 配置与选项
.net·.netcore·1024程序员节
mudtools2 小时前
在飞书 SDK 上用 Microsoft Agent Framework 搭一个能干活的工作搭子
人工智能·ai·.net·飞书·agent
ChaITSimpleLove2 小时前
Layout.WatchDog.NET 1.5.0-beta.3 发布:统一全后端雪花 ID 策略,数据库能力矩阵一览
.net·watchdog·日志记录器·节点资源监控看板·.net专属轻量级日志中心
IT规划师3 小时前
MQTT 实战 - 放下炫技,8 年踩坑的真心话与方法论(收官)
物联网·.net
CSharp精选营3 小时前
.NET 接口从 800ms 干到 60ms:生产环境这几个优化点最关键
性能优化·c#·.net·efcore
CSharp精选营1 天前
制造业 ERP 物料建模:主料、辅料和 BOM 到底怎么拆才稳
.net·erp·bom·制造业·物料建模
玩泥巴的2 天前
在飞书 SDK 上用 Microsoft Agent Framework 搭一个能干活的工作搭子
ai·.net·飞书·agent
波力海苔夹心脆6752 天前
C# 委托与事件详解:委托声明、Action/Func、多播、event 与 EventHandler 入门教程
开发语言·c#·.net·上位机·委托·事件