在C#的高性能开发场景中,Span和Memory是处理内存操作的核心类型,它们能够在不引发额外堆内存分配的前提下,对连续内存区域进行安全访问,大幅降低内存开销与GC压力,是优化程序性能的重要工具。

Span和Memory的基本概念
Span<T>是一个值类型,它表示一个连续内存区域的切片,能够指向栈内存、堆内存或者非托管内存,生命周期受限于当前栈帧,不能存储在堆上的字段中。Memory<T>是一个引用类型,同样表示连续内存区域的切片,但支持存储在堆上,适合需要跨方法、跨异步操作传递内存切片的场景。
两者的核心优势在于避免了传统数组切片时的内存拷贝,比如从大数组中截取一段数据时,使用Span或Memory只需要记录原始内存的起始位置和长度,不需要创建新的数组实例。
Span的基础使用场景
数组切片操作
传统数组切片需要创建新数组并拷贝数据,而使用Span可以直接基于原数组生成切片,示例如下:
using System;
class Program
{
static void Main()
{
int[] sourceArray = { 1, 2, 3, 4, 5, 6, 7, 8 };
// 基于原数组创建Span,截取索引2到索引5(共4个元素)的切片
Span<int> sliceSpan = sourceArray.AsSpan(2, 4);
Console.WriteLine("切片元素:");
foreach (int item in sliceSpan)
{
Console.Write(item + " ");
}
// 输出:3 4 5 6
// 修改切片元素会影响原数组
sliceSpan[0] = 100;
Console.WriteLine("n修改后原数组第三个元素:" + sourceArray[2]); // 输出:100
}
}
栈内存操作
Span支持指向栈上的内存,比如使用stackalloc分配栈内存,适合短生命周期的小内存操作,避免堆分配:
using System;
class Program
{
static void Main()
{
// 在栈上分配10个int类型的内存,不需要GC回收
Span<int> stackSpan = stackalloc int[10];
for (int i = 0; i < stackSpan.Length; i++)
{
stackSpan[i] = i * 2;
}
Console.WriteLine("栈内存元素:");
foreach (int item in stackSpan)
{
Console.Write(item + " ");
}
// 输出:0 2 4 6 8 10 12 14 16 18
}
}
Memory的适用场景
当需要将内存切片传递到堆存储、异步方法或者作为类的字段时,就需要使用Memory,因为Span是值类型且依赖栈生命周期,无法跨这些场景使用。示例如下:
using System;
using System.Threading.Tasks;
class MemoryDemo
{
// Memory可以作为类的字段存储
private Memory<byte> _buffer;
public MemoryDemo(byte[] source)
{
_buffer = source.AsMemory();
}
// 异步方法中使用Memory,Span无法作为异步方法的参数
public async Task ProcessAsync()
{
// 模拟异步操作,处理内存切片
await Task.Delay(100);
Memory<byte> slice = _buffer.Slice(0, 5);
Console.WriteLine("Memory切片长度:" + slice.Length);
}
}
class Program
{
static async Task Main()
{
byte[] data = new byte[20];
MemoryDemo demo = new MemoryDemo(data);
await demo.ProcessAsync();
}
}
Span和Memory的核心差异对比
两者的核心差异如下表所示:
| 对比维度 | Span<T> | Memory<T> |
|---|---|---|
| 类型 | 值类型 | 引用类型 |
| 生命周期 | 受限于当前栈帧 | 可存储在堆上,生命周期不受栈限制 |
| 适用场景 | 同步方法内的内存操作、栈内存访问 | 跨方法传递、异步操作、类字段存储 |
| 是否支持异步 | 不支持 | 支持 |
高性能内存操作的注意事项
- 优先使用Span处理同步场景下的内存切片,减少不必要的堆分配。
- 需要跨异步方法、作为字段存储时,再选择Memory,避免不必要的引用类型开销。
- 避免将Span装箱,装箱会导致Span被包装到堆对象中,失去其高性能优势。
- 操作非托管内存时,需要配合对应的内存管理机制,避免内存泄漏。
Span和Memory的设计目标是让开发者在不牺牲安全性的前提下,获得接近原生内存操作的性能,合理使用这两个类型可以大幅降低程序的GC压力和内存占用。