在现代 C# 开发中,性能优化越来越重要,尤其是在处理大量数据、算法优化(如 DP)、或高频调用场景下。Span<T> 正是 .NET 为解决“高性能 + 内存安全”问题引入的核心工具之一。
本文将从 概念、原理、使用方式以及适用场景 全面讲解 Span<T>。
🧠 一、什么是 Span<T>?
简单来说:
Span<T> 是对一段连续内存的“视图(view)”,而不是数据本身。
你可以把它理解为:
Span<T> = 指针 + 长度
它并不拥有数据,而只是“引用”某一段数据。
📦 二、一个直观例子
int[] arr = {1, 2, 3, 4, 5};
Span<int> span = arr;
此时:
arr→ 真正的数据存储span→ 指向这段数据的一个窗口
🔍 三、Span 的核心特性
1️⃣ 零拷贝(Zero Copy)
Span<int> s = arr;
不会创建新数组,只是引用原始数据。
2️⃣ 支持高效切片
var sub = s[1..4];
这个操作不会复制数据,只是:
- 改变起始指针
- 修改长度
3️⃣ 修改会影响原数据
s[0] = 999;
Console.WriteLine(arr[0]); // 输出 999
因为它们共享同一块内存。
4️⃣ 高性能(关键优势)
- ❌ 无额外内存分配
- ❌ 无 GC 压力
- ✅ cache 友好
- ✅ 非常适合算法优化
5️⃣ 栈上结构(重要限制)
Span<T> 是一个:
ref struct
因此它有一些限制:
- ❌ 不能作为类的字段
- ❌ 不能用于 async / await
- ❌ 不能跨方法长期保存
👉 目的:避免“悬空引用”,保证内存安全
⚠️ 四、为什么 Span 比数组切片更高效?
❌ 数组切片(会复制)
var head = arr[..3];
等价于:
var head = new int[] {1, 2, 3};
👉 创建新数组 + 拷贝数据
✅ Span 切片(不复制)
var head = arr.AsSpan()[..3];
👉 只是改变“视图范围”
📊 五、对比总结
| 类型 | 是否复制 | 是否分配内存 | 性能 |
|---|---|---|---|
int[] | ❌ | ❌ | 一般 |
arr[..] | ✅ | ✅ | 较慢 |
Span<T> | ❌ | ❌ | 🚀 极快 |
🚀 六、常见使用方式
✔️ 从数组创建
Span<int> s = arr;
✔️ 切片操作
var sub = s[2..5];
✔️ 遍历
foreach (var x in s)
{
Console.WriteLine(x);
}
✔️ 高性能替代写法
❌ 普通写法:
var temp = arr[..10];
✅ 推荐写法:
var temp = arr.AsSpan()[..10];
🔥 七、适用场景
Span 非常适合以下场景:
✔️ 算法与数据结构
- 动态规划(DP)
- 滑动窗口
- 前缀和
✔️ 高频循环
- 避免 GC
- 提升缓存命中率
✔️ 字符串/数组处理
- 子串操作
- 数据解析
🧠 八、Span vs Memory
| 类型 | 是否在栈 | 是否支持 async |
Span<T> | ✅ | ❌ |
Memory<T> | ❌ | ✅ |
👉 简单记:
- 同步高性能 → Span
- 异步场景 → Memory
🧠 九、底层理解(进阶)
Span<T> 本质上类似于 C/C++ 的:
T* pointer + length
但相比指针:
- ✅ 有边界检查
- ✅ 类型安全
- ✅ 不会悬空引用
👉 可以理解为:“安全版指针”
📌 十、总结
Span<T> 是 C# 中用于高性能编程的核心工具之一,通过“零拷贝视图”实现高效内存操作。
记住这三点就够了:
Span不存数据,只是视图- 切片不会复制(零开销)
- 适用于性能敏感代码
