在C#的高性能开发场景中,Span和Memory是优化内存操作的核心类型,它们能够在不触发额外堆内存分配的情况下,高效操作连续内存区域,大幅降低GC压力,提升程序运行效率。

Span和Memory的核心概念
Span<T>是一个值类型,它表示一个连续内存区域的切片,可以指向栈内存、堆内存或者非托管内存,不需要额外的堆分配,因此不会产生GC开销。它的生命周期受限于当前栈帧,不能存储在堆上的字段中。
Memory<T>是一个引用类型,同样表示连续内存区域的切片,但是它可以存储在堆上,生命周期不受栈帧限制,适合需要跨方法传递内存切片的场景。
两者的核心区别
| 特性 | Span<T> | Memory<T> |
|---|---|---|
| 类型 | 值类型 | 引用类型 |
| 内存位置 | 只能指向栈、堆或非托管内存,不能存储在堆字段 | 可以存储在堆字段,支持跨方法传递 |
| GC影响 | 无额外堆分配,无GC压力 | 本身有少量堆分配,但操作的内存区域无额外分配 |
| 适用场景 | 方法内部临时内存操作 | 需要传递的内存切片场景 |
用Span优化字符串操作减少GC
传统的字符串截取操作会生成新的字符串对象,触发堆分配,而使用Span可以避免这个问题。比如我们需要从一个长字符串中提取子串进行处理,不需要生成新的字符串实例。
using System;
class Program
{
static void Main()
{
string longStr = "HelloWorldCSharpSpanTest";
// 传统方式截取子串,会生成新的字符串对象,触发堆分配
string subStr1 = longStr.Substring(5, 5);
Console.WriteLine($"传统方式截取结果:{subStr1}");
// 使用Span截取,不生成新字符串,无额外堆分配
ReadOnlySpan<char> span = longStr.AsSpan();
ReadOnlySpan<char> subSpan = span.Slice(5, 5);
// 直接基于Span处理,避免字符串分配
Console.WriteLine($"Span方式截取结果:{subSpan.ToString()}");
}
}
上面的代码中,ReadOnlySpan<char>基于原字符串的内存区域创建切片,Slice操作只是记录内存的起始位置和长度,不会复制数据,也不会生成新的堆对象,相比Substring方法大幅减少了内存分配。
用Span处理数组减少内存复制
当我们需要操作数组的一部分数据时,传统方式往往需要复制数组片段到新数组,而Span可以直接基于原数组创建切片,避免复制操作。
using System;
class Program
{
static void Main()
{
int[] sourceArray = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 传统方式获取数组片段,需要复制数据到新数组
int[] subArray1 = new int[3];
Array.Copy(sourceArray, 2, subArray1, 0, 3);
Console.WriteLine($"传统方式数组片段:{string.Join(",", subArray1)}");
// 使用Span获取数组片段,无复制,无额外分配
Span<int> arraySpan = sourceArray.AsSpan();
Span<int> subSpan = arraySpan.Slice(2, 3);
Console.WriteLine($"Span方式数组片段:{string.Join(",", subSpan.ToArray())}");
// 修改Span内的数据会直接影响原数组
subSpan[0] = 100;
Console.WriteLine($"修改后原数组对应位置的值:{sourceArray[2]}");
}
}
可以看到,通过Span操作数组片段,不仅避免了数组复制的开销,还能直接修改原数组的数据,适合需要频繁操作数组局部数据的场景。
Memory跨方法传递内存切片
如果需要在多个方法之间传递内存切片,Span因为生命周期限制无法存储在堆上,这时候就需要使用Memory。Memory可以转换为Span来进行具体操作。
using System;
class Program
{
static void Main()
{
int[] data = { 10, 20, 30, 40, 50 };
// 创建Memory切片
Memory<int> memory = data.AsMemory(1, 3);
// 跨方法传递Memory
ProcessMemory(memory);
Console.WriteLine($"处理后原数组的值:{string.Join(",", data)}");
}
static void ProcessMemory(Memory<int> memory)
{
// 将Memory转换为Span进行操作
Span<int> span = memory.Span;
for (int i = 0; i < span.Length; i++)
{
span[i] *= 2;
}
}
}
这个示例中,Memory可以安全地作为参数传递给ProcessMemory方法,在方法内部转换为Span进行操作,既满足了跨方法传递的需求,又能享受到Span无额外分配的优势。
性能对比与注意事项
在处理大量临时数据、字符串解析、网络数据缓冲区操作等场景中,使用Span和Memory可以大幅减少GC触发次数。比如循环解析10000次字符串截取操作,传统方式可能会触发多次GC,而使用Span则几乎不会产生GC开销。
需要注意的点:
- Span不能存储在类的字段中,也不能作为异步方法的参数,因为它的生命周期仅限于当前栈帧
- 操作Span和Memory时要确保切片的范围不超过原内存区域的长度,避免越界异常
- 对于需要长期持有的内存切片,使用Memory而不是Span
合理使用Span和Memory,能够在不改变原有业务逻辑的前提下,有效降低程序的内存分配量,减少GC压力,进而提升整体运行性能,是C#进阶开发中必备的优化技巧。