C# 8 引入了一个非常优雅的语法糖:Range / Index(切片语法)。
你可能已经用过:
var sub = arr[1..^1];
但它到底是怎么工作的?
👉 是语法糖?会不会复制?性能如何?
这篇文章帮你彻底搞懂它的底层逻辑。
🧠 一、两个核心类型
C# 切片语法背后,其实是两个结构体:
🔹 Index(索引)
Index i = 3; // 从前往后
Index j = ^1; // 从后往前(倒数第一个)
👉 ^ 表示从末尾开始计数
等价于:
new Index(1, fromEnd: true)
🔹 Range(范围)
Range r = 1..5;
👉 表示区间 [1, 5)(左闭右开)
📦 二、语法糖展开(核心原理)
✨ 写法
var sub = arr[1..4];
🔍 编译器会做什么?
等价于(概念上):
var range = new Range(new Index(1), new Index(4));
var sub = arr.Slice(range); // 或类似逻辑
👉 关键点:
Range只是描述范围- 真正行为由类型决定(数组 / Span / string)
⚠️ 三、数组 vs Span(本质区别)
这是最重要的部分👇
❌ 数组切片(会复制)
var sub = arr[1..4];
👉 实际行为:
var sub = new int[3];
Array.Copy(arr, 1, sub, 0, 3);
✔️ 新数组
✔️ 数据复制
❌ 有性能开销
✅ Span 切片(零拷贝)
var sub = arr.AsSpan()[1..4];
👉 实际行为:
var sub = span.Slice(1, 3);
✔️ 不创建新数组
✔️ 不复制数据
✔️ 只是改变视图范围
📊 四、图解理解(非常重要)
原数组
arr: [0][1][2][3][4][5]
↑
❌ arr[1..4]
new array:
[1][2][3]
👉 完整复制
✅ Span
pointer → arr[1]
length → 3
👉 只是“看”这一段
🚀 五、常见写法总结
✔️ 基础切片
arr[0..3] // 前3个
arr[3..] // 从3到结尾
arr[..3] // 从头到3
arr[..] // 整个数组(复制)
✔️ 使用 ^(从后往前)
arr[^1] // 最后一个
arr[^3..] // 最后三个
arr[..^1] // 去掉最后一个
arr[1..^1] // 去头去尾
⚡ 六、性能分析(关键)
| 写法 | 是否复制 | 是否分配内存 | 推荐 |
|---|---|---|---|
arr[..] | ✅ | ✅ | ❌ |
arr[1..4] | ✅ | ✅ | ❌ |
Span[..] | ❌ | ❌ | ✅ |
👉 结论:
Range 是语法糖,性能取决于底层类型!
🧠 七、为什么设计成这样?
很多人会问:
👉 为什么数组不直接支持“零拷贝切片”?
原因是:
❌ 如果数组共享内存:
- 修改一个影响另一个
- GC 无法正确管理
- 容易产生隐式副作用
✅ 所以设计为:
- 数组 → 安全(复制)
- Span → 高性能(共享)
👉 这是一个“安全 vs 性能”的设计平衡
🔥 八、底层方法(进阶)
你可以手动使用:
arr.AsSpan().Slice(start, length);
👉 这其实就是 Range 的底层实现方式
🧠 九、一个关键认知(很多人不知道)
👉 Range 本身:
- ❌ 不操作数据
- ❌ 不切片
- ✅ 只是一个“描述区间的结构体”
真正的行为由:
arraySpanstring
来决定
🚀 十、实战建议
✔️ 普通业务代码
var sub = arr[1..4];
👉 可读性优先
✔️ 算法 / 性能优化(你现在重点)
var sub = arr.AsSpan()[1..4];
👉 零拷贝 + 高性能
✔️ 极致优化
Span<int> s = arr;
var sub = s[1..4];
📌 十一、总结
Range / Index 是语法糖,本身不做任何数据操作,真正的行为由底层类型决定。
记住这三点:
..= Range(范围)^= 从末尾计数- 数组切片会复制,Span 不会
🧠 最终一句话
👉 Range 是“描述”,Span 才是“性能”
