索引和范围Indices and ranges

范围和索引为访问序列中的单个元素或范围提供了简洁的语法。Ranges and indices provide a succinct syntax for accessing single elements or ranges in a sequence.

在本教程中,你将了解:In this tutorial, you'll learn how to:

  • 对某个序列中的范围使用该语法。Use the syntax for ranges in a sequence.
  • 了解每个序列开头和末尾的设计决策。Understand the design decisions for the start and end of each sequence.
  • 了解 IndexRange 类型的应用场景。Learn scenarios for the Index and Range types.

对索引和范围的语言支持Language support for indices and ranges

此语言支持依赖于两个新类型和两个新运算符:This language support relies on two new types and two new operators:

  • System.Index 表示一个序列索引。System.Index represents an index into a sequence.
  • 来自末尾运算符 ^ 的索引,指定一个索引与序列末尾相关。The index from end operator ^, which specifies that an index is relative to the end of a sequence.
  • System.Range 表示序列的子范围。System.Range represents a sub range of a sequence.
  • 范围运算符 ..,用于指定范围的开始和末尾,就像操作数一样。The range operator .., which specifies the start and end of a range as its operands.

让我们从索引规则开始。Let's start with the rules for indices. 请考虑数组 sequenceConsider an array sequence. 0 索引与 sequence[0] 相同。The 0 index is the same as sequence[0]. ^0 索引与 sequence[sequence.Length] 相同。The ^0 index is the same as sequence[sequence.Length]. 表达式 sequence[^0] 不会引发异常,就像 sequence[sequence.Length] 一样。The expression sequence[^0] does throw an exception, just as sequence[sequence.Length] does. 对于任何数字 n,索引 ^nsequence[sequence.Length - n] 相同。For any number n, the index ^n is the same as sequence[sequence.Length - n].

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

可以使用 ^1 索引检索最后一个词。You can retrieve the last word with the ^1 index. 在初始化下面添加以下代码:Add the following code below the initialization:

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

范围指定范围的开始和末尾。A range specifies the start and end of a range. 范围是排除的,也就是说“末尾”不包含在范围内。Ranges are exclusive, meaning the end isn't included in the range. 范围 [0..^0] 表示整个范围,就像 [0..sequence.Length] 表示整个范围。The range [0..^0] represents the entire range, just as [0..sequence.Length] represents the entire range.

以下代码创建了一个包含单词“quick”、“brown”和“fox”的子范围。The following code creates a subrange with the words "quick", "brown", and "fox". 它包括 words[1]words[3]It includes words[1] through words[3]. 元素 words[4] 不在该范围内。The element words[4] isn't in the range. 将以下代码添加到同一方法中。Add the following code to the same method. 将其复制并粘贴到交互式窗口的底部。Copy and paste it at the bottom of the interactive window.

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

以下代码使用“lazy”和“dog”返回范围。The following code returns the range with "lazy" and "dog". 它包括 words[^2]words[^1]It includes words[^2] and words[^1]. 结束索引 words[^0] 不包括在内。The end index words[^0] isn't included. 同样添加以下代码:Add the following code as well:

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

下面的示例为开始和/或结束创建了开放范围:The following examples create ranges that are open ended for the start, end, or both:

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();

还可以将范围或索引声明为变量。You can also declare ranges or indices as variables. 然后可以在 [] 字符中使用该变量:The variable can then be used inside the [ and ] characters:

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();

下面的示例展示了使用这些选项的多种原因。The following sample shows many of the reasons for those choices. 请修改 xyz 以尝试不同的组合。Modify x, y, and z to try different combinations. 在进行实验时,请使用 x 小于 yy 小于 z 的有效组合值。When you experiment, use values where x is less than y, and y is less than z for valid combinations. 在新方法中添加以下代码。Add the following code in a new method. 尝试不同的组合:Try different combinations:

int[] numbers = Enumerable.Range(0, 100).ToArray();
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]}");

索引和范围的类型支持Type support for indices and ranges

索引和范围提供清晰、简洁的语法来访问序列中的单个元素或元素的范围。Indexes and ranges provide clear, concise syntax to access a single element or a range of elements in a sequence. 索引表达式通常返回序列元素的类型。An index expression typically returns the type of the elements of a sequence. 范围表达式通常返回与源序列相同的序列类型。A range expression typically returns the same sequence type as the source sequence.

若任何类型提供带 IndexRange 参数的索引器,则该类型可分别显式支持索引或范围。Any type that provides an indexer with an Index or Range parameter explicitly supports indices or ranges respectively. 采用单个 Range 参数的索引器可能会返回不同的序列类型,如 System.Span<T>An indexer that takes a single Range parameter may return a different sequence type, such as System.Span<T>.

重要

使用范围运算符的代码的性能取决于序列操作数的类型。The performance of code using the range operator depends on the type of the sequence operand.

范围运算符的时间复杂度取决于序列类型。The time complexity of the range operator depends on the sequence type. 例如,如果序列是 string 或数组,则结果是输入中指定部分的副本,因此,时间复杂度为 O(N)(其中 N 是范围的长度)。For example, if the sequence is a string or an array, then the result is a copy of the specified section of the input, so the time complexity is O(N) (where N is the length of the range). 另一方面,如果它是 System.Span<T>System.Memory<T>,则结果引用相同的后备存储,这意味着没有副本且操作为 O(1)。On the other hand, if it's a System.Span<T> or a System.Memory<T>, the result references the same backing store, which means there is no copy and the operation is O(1).

除了时间复杂度外,这还会产生额外的分配和副本,从而影响性能。In addition to the time complexity, this causes extra allocations and copies, impacting performance. 在性能敏感的代码中,考虑使用 Span<T>Memory<T> 作为序列类型,因为不会为其分配范围运算符。In performance sensitive code, consider using Span<T> or Memory<T> as the sequence type, since the range operator does not allocate for them.

若类型包含名称为 LengthCount 的属性,属性有可访问的 Getter 并且其返回类型为 int,则此类型为可计数类型。A type is countable if it has a property named Length or Count with an accessible getter and a return type of int. 不显式支持索引或范围的可计数类型可能为它们提供隐式支持。A countable type that doesn't explicitly support indices or ranges may provide an implicit support for them. 有关详细信息,请参阅功能建议说明隐式索引支持隐式范围支持部分。For more information, see the Implicit Index support and Implicit Range support sections of the feature proposal note. 使用隐式范围支持的范围将返回与源序列相同的序列类型。Ranges using implicit range support return the same sequence type as the source sequence.

例如,以下 .NET 类型同时支持索引和范围:StringSpan<T>ReadOnlySpan<T>For example, the following .NET types support both indices and ranges: String, Span<T>, and ReadOnlySpan<T>. List<T> 支持索引,但不支持范围。The List<T> supports indices but doesn't support ranges.

Array 具有更多的微妙行为。Array has more nuanced behavior. 单个维度数组同时支持索引和范围。Single dimension arrays support both indices and ranges. 多维数组则不支持。Multi-dimensional arrays don't. 多维数组的索引器具有多个参数,而不是一个参数。The indexer for a multi-dimensional array has multiple parameters, not a single parameter. 交错数组(也称为数组的数组)同时支持范围和索引器。Jagged arrays, also referred to as an array of arrays, support both ranges and indexers. 下面的示例演示如何循环访问交错数组的矩形子节。The following example shows how to iterate a rectangular subsection of a jagged array. 它循环访问位于中心的节,不包括前三行和后三行,以及每个选定行中的前两列和后两列:It iterates the section in the center, excluding the first and last three rows, and the first and last two columns from each selected row:

var jagged = new int[10][]
{
   new int[10] { 0,  1, 2, 3, 4, 5, 6, 7, 8, 9},
   new int[10] { 10,11,12,13,14,15,16,17,18,19},
   new int[10] { 20,21,22,23,24,25,26,27,28,29},
   new int[10] { 30,31,32,33,34,35,36,37,38,39},
   new int[10] { 40,41,42,43,44,45,46,47,48,49},
   new int[10] { 50,51,52,53,54,55,56,57,58,59},
   new int[10] { 60,61,62,63,64,65,66,67,68,69},
   new int[10] { 70,71,72,73,74,75,76,77,78,79},
   new int[10] { 80,81,82,83,84,85,86,87,88,89},
   new int[10] { 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 的范围运算符都会分配一个数组来存储返回的元素。In all cases, the range operator for Array allocates an array to store the elements returned.

索引和范围的应用场景Scenarios for indices and ranges

要分析较大序列的一部分时,通常会使用范围和索引。You'll often use ranges and indices when you want to analyze a portion of a larger sequence. 在准确读取所涉及的序列部分这一方面,新语法更清晰。The new syntax is clearer in reading exactly what portion of the sequence is involved. 本地函数 MovingAverageRange 为参数。The local function MovingAverage takes a Range as its argument. 然后,该方法在计算最小值、最大值和平均值时仅枚举该范围。The method then enumerates just that range when calculating the min, max, and average. 在项目中尝试以下代码:Try the following code in your project:

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)).ToArray();