[C#] C# 索引和范围运算符解释

英文原文: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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值