简介
.NET 中的
Span<T> 和 Memory<T> 是 .NET Core 2.1(对应 C# 7.2)引入的高性能内存访问类型,它们的主要目的是提供零拷贝、类型安全且连续内存区域的视图(view),避免不必要的数组复制和堆分配,从而显著提升性能,尤其适合解析、序列化、网络 IO、字符串处理等场景。为什么需要 Span<T> 和 Memory<T>?
- 🚀 零分配:直接引用现有数据,无需复制
- ⚡ 高性能:减少 GC 压力和 CPU 缓存失效
- 🔒 内存安全:编译器支持的引用语义验证
- 🎯 统一接口:支持数组、本机指针、栈数据等多种数据源
场景与问题
考虑这样的场景:你需要处理一个大数组的一部分
// 传统方式 int[] largeArray = new int[1000000]; int[] subArray = new int[100]; Array.Copy(largeArray, 0, subArray, 0, 100); // 内存分配 + 数据复制 ProcessArray(subArray);
问题:
- 每次创建新数组都会分配堆内存
- 数据复制开销大
- GC 压力增加
- 对于高性能场景不可接受
Span<T> 详解
定义与特性
Span<T> 是一个值类型(struct),它是对连续内存区域的轻量级引用,大小仅为 16 字节(64 位系统)。public readonly ref struct Span<T> { public int Length { get; } public ref T this[int index] { get; } // 其他成员... }
关键特性:
- ✅ 值类型(struct)— 分配在栈上
- ✅ 包含指针和长度信息
- ✅ 支持任意内存源(栈、堆、非托管内存)
- ❌ 不能用于异步方法(ref struct 限制)
- ❌ 不能装箱
- ❌ 不能作为类型参数
使用示例
// 1. 从数组创建 int[] array = { 1, 2, 3, 4, 5 }; Span<int> span = array.AsSpan(); Span<int> slice = span.Slice(1, 3); // [2, 3, 4] // 2. 从栈数据创建 Span<int> stackSpan = stackalloc int[10]; for (int i = 0; i < 10; i++) stackSpan[i] = i; // 3. 从指针创建 unsafe { fixed (int* ptr = array) { Span<int> ptrSpan = new Span<int>(ptr, array.Length); } } // 4. 切片操作 — 零分配 Span<int> part = span[1..4]; // C# 8.0+ 范围语法
Memory<T> 详解
定义与特性
Memory<T> 是一个值类型,但不是 ref struct。它对应用于存储对内存的引用,并且可以在异步方法中使用。public readonly struct Memory<T> { public int Length { get; } public Span<T> Span { get; } public MemoryPool<T> MemoryPool { get; } // 其他成员... }
关键特性:
- ✅ 可用于异步方法
- ✅ 可以装箱
- ✅ 可作为泛型参数
- ✅ 支持 IDisposable 的生命周期管理
- ❌ 相对于 Span<T> 有额外的堆分配(在某些情况下)
使用示例
// 1. 从数组创建 int[] array = { 1, 2, 3, 4, 5 }; Memory<int> memory = array.AsMemory(); Memory<int> slice = memory.Slice(1, 3); // 2. 异步操作 async Task ProcessAsync(Memory<int> data) { await Task.Delay(100); Span<int> span = data.Span; // 获取 Span ProcessSpan(span); } // 3. 获取可释放的内存池 using (var buffer = MemoryPool<byte>.Shared.Rent(1024)) { Memory<byte> memory = buffer.Memory; // 使用 memory }
与 Span<T> 的关系
Memory<int> memory = new int[10].AsMemory(); Span<int> span = memory.Span; // Memory → Span(隐式转换) // 反向不可以 // Span<int> span = stackalloc int[10]; // Memory<int> memory = span.AsMemory(); // ❌ 编译错误
ReadOnlySpan<T> 和 ReadOnlyMemory<T>
只读变体
.NET 同时提供了只读版本,用于表示不可修改的内存区域。
// ReadOnlySpan<T> ReadOnlySpan<char> text = "Hello".AsSpan(); // text[0] = 'A'; // ❌ 编译错误 // ReadOnlyMemory<T> ReadOnlyMemory<byte> data = Encoding.UTF8.GetBytes("Test").AsMemory();
适用场景:
- 只读数据(字符串、常量)
- 参数验证(确保调用者不修改数据)
- API 设计(明确表示意图)
关键区别
特性 | Span<T> | Memory<T> | ReadOnlySpan<T> | ReadOnlyMemory<T> |
是否为 ref struct | ✅ 是 | ❌ 否 | ✅ 是 | ❌ 否 |
可用于异步 | ❌ 否 | ✅ 是 | ❌ 否 | ✅ 是 |
可装箱 | ❌ 否 | ✅ 是 | ❌ 否 | ✅ 是 |
可作为泛型参数 | ❌ 否 | ✅ 是 | ❌ 否 | ✅ 是 |
内存开销 | 16 字节 | 16+ 字节 | 16 字节 | 16+ 字节 |
支持切片 | ✅ 是 | ✅ 是 | ✅ 是 | ✅ 是 |
零分配 | ✅ 是* | ⚠️ 视情况 | ✅ 是* | ⚠️ 视情况 |
- Stack allocated 数据的 Span 通常为零分配;托管堆数据通过 AsSpan() 也是零分配
实际应用场景
场景 1:文本处理
// 解析 CSV 一行数据,无分配 void ParseCsvLine(ReadOnlySpan<char> line) { var fields = line.Split(','); foreach (var field in fields) { var trimmed = field.Trim(); // 处理字段... } } // 调用 ParseCsvLine("John,Doe,30"); // 无数组分配
场景 2:网络 I/O 优化
// 接收网络数据 public async Task ReceiveDataAsync(NetworkStream stream, Memory<byte> buffer) { int bytesRead = await stream.ReadAsync(buffer); ProcessData(buffer.Span.Slice(0, bytesRead)); } // ProcessData 无需关心数据来源,性能一致 void ProcessData(ReadOnlySpan<byte> data) { for (int i = 0; i < data.Length; i++) { // 处理每个字节 } }
场景 3:栈分配优化
// 处理小数据集,使用栈分配 void ProcessSmallArray() { Span<int> buffer = stackalloc int[256]; for (int i = 0; i < buffer.Length; i++) { buffer[i] = i * 2; } // buffer 自动释放,无 GC 压力 }
场景 4:异步数据处理
// 需要在异步方法中使用,必须用 Memory<T> async Task ProcessLargeFileAsync(string filePath) { using var file = File.OpenRead(filePath); byte[] buffer = new byte[4096]; Memory<byte> memory = buffer.AsMemory(); int bytesRead; while ((bytesRead = await file.ReadAsync(memory)) > 0) { await HandleChunkAsync(memory.Slice(0, bytesRead)); } }
前后对比
// 传统方式:每次分配新数组 void TraditionalApproach(int[] data) { for (int i = 0; i < data.Length - 10; i++) { int[] slice = new int[10]; Array.Copy(data, i, slice, 0, 10); ProcessArray(slice); } } // Span 方式:零分配切片 void SpanApproach(int[] data) { Span<int> span = data.AsSpan(); for (int i = 0; i < span.Length - 10; i++) { var slice = span.Slice(i, 10); ProcessSpan(slice); } }
最佳实践
✅ 推荐做法
- 使用 Span<T> 作为参数
// 好:接受任意数据源 void Process(ReadOnlySpan<int> data) { }
- 优先使用 ReadOnlySpan<T>/ReadOnlyMemory<T>
// 比 Span\<T\> 更安全,表意更明确 void ValidateInput(ReadOnlySpan<byte> input) { }
- 异步方法中使用 Memory<T>
async Task HandleAsync(Memory<byte> buffer) { }
- 小缓冲区使用 stackalloc
Span<byte> buffer = stackalloc byte[256];
❌ 避免的做法
- 不要在 ref struct 字段中存储引用
// ❌ 编译错误 ref struct Container { Span<int> data; // ref struct 不能包含引用字段 }
- 不要在异步方法中使用 Span<T>
// ❌ 编译错误 async Task BadAsync(Span<int> data) { }
- 小心栈溢出
// ❌ 危险:分配过大 Span<byte> huge = stackalloc byte[10_000_000];
常见问题
Q1: 何时使用 Span<T> vs Memory<T>?
使用 Span<T>:
- 同步 API
- 不需要存储引用
- 追求最小开销
使用 Memory<T>:
- 异步 API
- 需要跨方法边界传递
- 与 IAsyncEnumerable 或任务配合
Q2: Span<T> 为什么是 ref struct?
ref struct 的限制保证了:
- 栈数据不会逃逸到堆
- 不会发生悬垂指针
- 引用总是有效的
Q3: 如何与现有代码兼容?
.NET 提供了隐式转换:
int[] array = new int[10]; Span<int> span = array; // 隐式转换
总结
Span<T> 和 Memory<T> 是现代 .NET 性能优化的核心工具:关键点 | 说明 |
核心价值 | 零分配、零开销的内存操作 |
Span | 栈数据、同步 API、最高性能 |
Memory | 异步 API、可存储、灵活性高 |
ReadOnly 变体 | 安全性、意图清晰、接口设计 |
应用场景 | 文本处理、网络 I/O、数据处理、性能优化 |
掌握这两个类型,你将能够编写出更高效、更符合现代 .NET 最佳实践的代码。
