英文原文:https://blog.ndepend.com/c-index-and-range-operators-explained/
在这篇文章中,我们将最全面地揭开 C# 索引 ^ 和范围 … 运算符的神秘面纱。
索引运算符 ^
让我们从索引 ^ 运算符开始:
// 用于本文所有演示的数组
var arr = new[] { 0, 1, 2, 3, 4, 5 };
Assert.IsTrue(arr.Length == 6);
// 只是为了澄清 index 和 arr[index] 相等
for (var i = 0; i < arr.Length; i++) {
Assert.IsTrue(arr[i] == i);
}
// [^1] 表示最后一个元素
// equivalent to [arr.Length - 1]
Assert.IsTrue(arr[^1] == 5);
Assert.IsTrue(arr[arr.Length - 1] == 5);
// [^2] 表示倒数第二个元素
// equivalent to [arr.Length - 2] and so on
Assert.IsTrue(arr[^2] == 4);
for (var i = 0; i < arr.Length; i++) {
// // arr[^i] 从末尾开始索引为:arr[arr.Length -i]
Assert.IsTrue(arr[^(arr.Length - i)] == i);
Assert.IsTrue(arr[^(6 - i)] == i);
// 有点头疼……不过也有道理!
Assert.IsTrue(arr[^(i + 1)] == 5 - i);
}
// arr[^0] 表示最后一个元素之后的索引 arr[arr.Length]
// 并抛出 IndexOutOfRangeException
bool exThrown = false;
try { int i = arr[^0]; } catch (IndexOutOfRangeException) { exThrown = true; }
Assert.IsTrue(exThrown);
如果您习惯使用正则表达式 (regex),则此语法有点误导。在 C# 中,^ 运算符表示从末尾开始索引,而在正则表达式中,字符 ^ 匹配字符串中的起始位置。
范围运算符…
范围运算符 … 用于制作集合的切片。
// [..] 表示所有元素的范围
Assert.IsTrue(arr[..].SequenceEqual(arr));
// range [1..4] returns {1, 2, 3 }
// 包含范围的开头 (1)
// 不包含范围的结尾 (4)
Assert.IsTrue(arr[1..4].SequenceEqual(new [] { 1 , 2 , 3 }));
// [..3] 返回 { 0, 1, 2 } 从头到 3(不包括)
Assert.IsTrue(arr[..3].SequenceEqual(new [] { 0, 1, 2 }));
// [3..] returns { 3, 4, 5 } from 3 inclusive till the end
Assert.IsTrue(arr[3..].SequenceEqual(new [] { 3, 4, 5 }));
请记住:
- 范围的开始包含在内
- 范围的末尾是不包含在内的
如果您学过数学,那么这种语法有点误导。 C# 语法 [1…4] 转换为数学符号 [1…4[。
混合索引和范围运算符
两个运算符可以在同一表达式中混合使用:
// [0..^0] 表示从开始到结束
// 相当于[..]
// 请记住上限 ^0 是不包括的
// 所以这里不存在 IndexOutOfRangeException 的风险
Assert.IsTrue(arr[0..^0].SequenceEqual(arr));
// [2..^2] means [2..(6-2)] means [2..4]
Assert.IsTrue(arr[2..^2].SequenceEqual(new[] { 2, 3 }));
// [^4..^1] means [(6-4)..(6-1)] means [2..5]
Assert.IsTrue(arr[^4..^1].SequenceEqual(new[] { 2, 3, 4 }));
索引 ^ 运算符语法糖背后是什么?
实际上,C# 编译器将这些运算符转换为结构 System.Index 和 System.Range。这些结构是在 2019 年随 .NET Core 3.0 引入的。.NET Standard 2.1 也提供了这些结构,但 .NET Standard 2.0 不提供。这意味着您不能在 .NET Framework 项目中使用此语法。
以下是编译器对索引语法所做的操作:
Index lastIndex = ^1;
// translated to
lastIndex = new Index(1, true); // true means fromEnd: true
Assert.IsTrue(arr[^1] == 5);
// translated to
Assert.IsTrue(arr[lastIndex] == 5);
// translated to
Assert.IsTrue(arr[lastIndex.GetOffset(arr.Length)] == 5);
在简单的情况下,C#编译器不需要 Index 结构,并且可以在 IL 中完成从末尾开始的索引工作。
带索引的 CSharp 编译器优化
范围…运算符语法糖背后是什么?
我们已经提到了 Range 结构。以下是编译器如何使用它。请注意对特殊方法 T[] System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray(T[] array, Range range) 的调用。
Range range = 2..^1;
// translated to
range = new Range(2, ^1);
// translated to (note that the literal 2 is translated to new Index(2))
range = new Range(new Index(2), new Index(1, true));
Assert.IsTrue(arr[2..^1].SequenceEqual(new[] { 2, 3, 4 }));
// translates to
Assert.IsTrue(arr[range].SequenceEqual(new[] { 2, 3, 4 }));
// translates to
Assert.IsTrue(arr[new Range(
new Index(2),
new Index(1, true).GetOffset(arr.Length))]
.SequenceEqual(new[] { 2, 3, 4 }));
// translates to
Assert.IsTrue(
System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray(
arr,
new Range(
new Index(2),
new Index(1, true).GetOffset(arr.Length)))
.SequenceEqual(new[] {2, 3, 4}));
支持数组以外的集合
索引语法 ^ 适用于具有以下两者的所有集合类型:
- Count 或者 Length 属性
- 和一个整数索引器 [int]
正如我们所看到的,索引语法 ^ 适用于 IList<T> 和 List<T>,但不适用于 ISet<T>、Hashset<T>、IDictionary<K,V> 和 Dictionary<K,V>。最后四个不是索引集合。
范围语法 … 更具限制性,并且还要求集合类型提供 int[] Slice(int start, int length) 方法。因此,范围运算符无法处理这些集合中的任何一个。即使使用 IList<T> 和 List<T> 也不会像人们预期的那样。
向 IList<T> 和 List<T> 集合添加 Slice() 方法不是一个选项,因为在许多情况下这是一个重大更改。这是一个关于试图弥补这一缺陷的 Reddit 讨论。看起来使用默认接口实现语法向 IList<T> 添加 Slice() 方法可能是避免重大更改的选项。我没有找到没有完成的原因,你知道吗?
支持数组以外的集合
最后,这是一个支持索引和范围语法的自定义集合的示例:
var myCollection = new MyCollection<int>(0, 1, 2, 3, 4, 5);
Assert.IsTrue(myCollection[^1] == 5);
Assert.IsTrue(myCollection[2..^1].SequenceEqual(new[] { 2, 3, 4 }));
class MyCollection<T> {
internal MyCollection(params T[] array) {
_array = array;
}
private T[] _array;
// Length or Count is required for both [^1] and [2..^1]
internal int Length => _array.Length;
// Int indexer is required for [^1] but not for [2..^1]
internal T this[int index] => _array[index];
// Slice is required for [2..^1] but not for [^1]
internal T[] Slice(int start, int length) {
var slice = new T[length];
Array.Copy(_array, start, slice, 0, length);
return slice;
}
}
结束语
就我个人而言,在使用语法糖之前,我需要彻底理解它,因此我写了这篇文章。
这种新语法很棒,但有点局限,因为 IList<T> 和 List<T> 不支持 range。