索引和范围

范围和索引为访问序列中的单个元素或范围提供了简洁的语法。

在本教程中,你将了解:

  • 对某个序列中的范围使用该语法。
  • 隐式定义 Range
  • 了解每个序列开头和末尾的设计决策。
  • 了解 IndexRange 类型的应用场景。

对索引和范围的语言支持

索引和范围为访问序列中的单个元素或范围提供了简洁的语法。

此语言支持依赖于两个新类型和两个新运算符:

让我们从索引规则开始。 请考虑数组 sequence0 索引与 sequence[0] 相同。 ^0 索引与 sequence[sequence.Length] 相同。 表达式 sequence[^0] 会引发异常,就像 sequence[sequence.Length] 一样。 对于任何数字 n,索引 ^nsequence.Length - n 相同。

string[] words = [
                // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumps",    // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
];              // 9 (or words.Length) ^0

可以使用 ^1 索引检索最后一个词。 在初始化下面添加以下代码:

Console.WriteLine($"The last word is {words[^1]}");

范围指定范围的开始和末尾。 包括此范围的开始,但不包括此范围的末尾,这表示此范围包含开始但不包含末尾 。 范围 [0..^0] 表示整个范围,就像 [0..sequence.Length] 表示整个范围。

以下代码创建了一个包含单词“quick”、“brown”和“fox”的子范围。 它包括 words[1]words[3]。 元素 words[4] 不在该范围内。

string[] quickBrownFox = words[1..4];
foreach (var word in quickBrownFox)
    Console.Write($"< {word} >");
Console.WriteLine();

以下代码使用“lazy”和“dog”返回范围。 它包括 words[^2]words[^1]。 结束索引 words[^0] 不包括在内。 同样添加以下代码:

string[] lazyDog = words[^2..^0];
foreach (var word in lazyDog)
    Console.Write($"< {word} >");
Console.WriteLine();

下面的示例为开始和/或结束创建了开放范围:

string[] allWords = words[..]; // contains "The" through "dog".
string[] firstPhrase = words[..4]; // contains "The" through "fox"
string[] lastPhrase = words[6..]; // contains "the", "lazy" and "dog"
foreach (var word in allWords)
    Console.Write($"< {word} >");
Console.WriteLine();
foreach (var word in firstPhrase)
    Console.Write($"< {word} >");
Console.WriteLine();
foreach (var word in lastPhrase)
    Console.Write($"< {word} >");
Console.WriteLine();

还可以将范围或索引声明为变量。 然后可以在 [] 字符中使用该变量:

Index the = ^3;
Console.WriteLine(words[the]);
Range phrase = 1..4;
string[] text = words[phrase];
foreach (var word in text)
    Console.Write($"< {word} >");
Console.WriteLine();

下面的示例展示了使用这些选项的多种原因。 请修改 xyz 以尝试不同的组合。 在进行实验时,请使用 x 小于 yy 小于 z 的有效组合值。 在新方法中添加以下代码。 尝试不同的组合:

int[] numbers = [..Enumerable.Range(0, 100)];
int x = 12;
int y = 25;
int z = 36;

Console.WriteLine($"{numbers[^x]} is the same as {numbers[numbers.Length - x]}");
Console.WriteLine($"{numbers[x..y].Length} is the same as {y - x}");

Console.WriteLine("numbers[x..y] and numbers[y..z] are consecutive and disjoint:");
Span<int> x_y = numbers[x..y];
Span<int> y_z = numbers[y..z];
Console.WriteLine($"\tnumbers[x..y] is {x_y[0]} through {x_y[^1]}, numbers[y..z] is {y_z[0]} through {y_z[^1]}");

Console.WriteLine("numbers[x..^x] removes x elements at each end:");
Span<int> x_x = numbers[x..^x];
Console.WriteLine($"\tnumbers[x..^x] starts with {x_x[0]} and ends with {x_x[^1]}");

Console.WriteLine("numbers[..x] means numbers[0..x] and numbers[x..] means numbers[x..^0]");
Span<int> start_x = numbers[..x];
Span<int> zero_x = numbers[0..x];
Console.WriteLine($"\t{start_x[0]}..{start_x[^1]} is the same as {zero_x[0]}..{zero_x[^1]}");
Span<int> z_end = numbers[z..];
Span<int> z_zero = numbers[z..^0];
Console.WriteLine($"\t{z_end[0]}..{z_end[^1]} is the same as {z_zero[0]}..{z_zero[^1]}");

不仅数组支持索引和范围。 还可以将索引和范围用于 stringSpan<T>ReadOnlySpan<T>

隐式范围运算符表达式转换

使用范围运算符表达式语法时,编译器会将开始值和结束值隐式转换为 Index,并根据这些值创建新的 Range 实例。 以下代码显示了范围运算符表达式语法的隐式转换示例及其对应的显式替代方法:

Range implicitRange = 3..^5;

Range explicitRange = new(
    start: new Index(value: 3, fromEnd: false),
    end: new Index(value: 5, fromEnd: true));

if (implicitRange.Equals(explicitRange))
{
    Console.WriteLine(
        $"The implicit range '{implicitRange}' equals the explicit range '{explicitRange}'");
}
// Sample output:
//     The implicit range '3..^5' equals the explicit range '3..^5'

重要

当值为负数时,从 Int32 隐式转换为 Index 会引发 ArgumentOutOfRangeException。 同样,当 value 参数为负时,Index 构造函数会引发 ArgumentOutOfRangeException

索引和范围的类型支持

索引和范围提供清晰、简洁的语法来访问序列中的单个元素或元素的范围。 索引表达式通常返回序列元素的类型。 范围表达式通常返回与源序列相同的序列类型。

若任何类型提供带 IndexRange 参数的索引器,则该类型可分别显式支持索引或范围。 采用单个 Range 参数的索引器可能会返回不同的序列类型,如 System.Span<T>

重要

使用范围运算符的代码的性能取决于序列操作数的类型。

范围运算符的时间复杂度取决于序列类型。 例如,如果序列是 string 或数组,则结果是输入中指定部分的副本,因此,时间复杂度为 O(N)(其中 N 是范围的长度)。 另一方面,如果它是 System.Span<T>System.Memory<T>,则结果引用相同的后备存储,这意味着没有副本且操作为 O(1)。

除了时间复杂度外,这还会产生额外的分配和副本,从而影响性能。 在性能敏感的代码中,考虑使用 Span<T>Memory<T> 作为序列类型,因为不会为其分配范围运算符。

若类型包含名称为 LengthCount 的属性,属性有可访问的 Getter 并且其返回类型为 int,则此类型为可计数类型。 不显式支持索引或范围的可计数类型可能为它们提供隐式支持。 有关详细信息,请参阅功能建议说明隐式索引支持隐式范围支持部分。 使用隐式范围支持的范围将返回与源序列相同的序列类型。

例如,以下 .NET 类型同时支持索引和范围:StringSpan<T>ReadOnlySpan<T>List<T> 支持索引,但不支持范围。

Array 具有更多的微妙行为。 单个维度数组同时支持索引和范围。 多维数组不支持索引器或范围。 多维数组的索引器具有多个参数,而不是一个参数。 交错数组(也称为数组的数组)同时支持范围和索引器。 下面的示例演示如何循环访问交错数组的矩形子节。 它循环访问位于中心的节,不包括前三行和后三行,以及每个选定行中的前两列和后两列:

int[][] jagged = 
[
   [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
   [10,11,12,13,14,15,16,17,18,19],
   [20,21,22,23,24,25,26,27,28,29],
   [30,31,32,33,34,35,36,37,38,39],
   [40,41,42,43,44,45,46,47,48,49],
   [50,51,52,53,54,55,56,57,58,59],
   [60,61,62,63,64,65,66,67,68,69],
   [70,71,72,73,74,75,76,77,78,79],
   [80,81,82,83,84,85,86,87,88,89],
   [90,91,92,93,94,95,96,97,98,99],
];

var selectedRows = jagged[3..^3];

foreach (var row in selectedRows)
{
    var selectedColumns = row[2..^2];
    foreach (var cell in selectedColumns)
    {
        Console.Write($"{cell}, ");
    }
    Console.WriteLine();
}

在所有情况下,Array 的范围运算符都会分配一个数组来存储返回的元素。

索引和范围的应用场景

要分析较大序列的一部分时,通常会使用范围和索引。 在准确读取所涉及的序列部分这一方面,新语法更清晰。 本地函数 MovingAverageRange 为参数。 然后,该方法在计算最小值、最大值和平均值时仅枚举该范围。 在项目中尝试以下代码:

int[] sequence = Sequence(1000);

for(int start = 0; start < sequence.Length; start += 100)
{
    Range r = start..(start+10);
    var (min, max, average) = MovingAverage(sequence, r);
    Console.WriteLine($"From {r.Start} to {r.End}:    \tMin: {min},\tMax: {max},\tAverage: {average}");
}

for (int start = 0; start < sequence.Length; start += 100)
{
    Range r = ^(start + 10)..^start;
    var (min, max, average) = MovingAverage(sequence, r);
    Console.WriteLine($"From {r.Start} to {r.End}:  \tMin: {min},\tMax: {max},\tAverage: {average}");
}

(int min, int max, double average) MovingAverage(int[] subSequence, Range range) =>
    (
        subSequence[range].Min(),
        subSequence[range].Max(),
        subSequence[range].Average()
    );

int[] Sequence(int count) => [..Enumerable.Range(0, count).Select(x => (int)(Math.Sqrt(x) * 100))];

关于范围索引和数组的说明

从数组中获取范围时,结果是从初始数组复制的数组,而不是引用的数组。 修改生成的数组中的值不会更改初始数组中的值。

例如:

var arrayOfFiveItems = new[] { 1, 2, 3, 4, 5 };

var firstThreeItems = arrayOfFiveItems[..3]; // contains 1,2,3
firstThreeItems[0] =  11; // now contains 11,2,3

Console.WriteLine(string.Join(",", firstThreeItems));
Console.WriteLine(string.Join(",", arrayOfFiveItems));

// output:
// 11,2,3
// 1,2,3,4,5

另请参阅