范围和索引提供了一种简洁的语法,用于访问序列中的单个元素或范围。
在本教程中,你将学习如何:
索引和范围的语言支持
索引和范围提供了一种简洁的语法,用于访问序列中的单个元素或范围。
此语言支持依赖于两个新类型和两个新运算符:
- System.Index 表示序列中的索引。
-
end 运算符
^中的索引,它指定索引相对于序列的末尾。 - System.Range 表示序列的子范围。
-
范围运算符
..,指定范围的开始和结束作为其作数。
让我们从索引的规则开始。 请考虑数组 sequence。 索引 0 与 sequence[0] 相同。 索引 ^0 和 sequence[sequence.Length] 是相同的。
sequence[^0]表达式会引发异常,就像sequence[sequence.Length]这样。 对于任何数字 n,索引 ^n 与 sequence.Length - n 相同。
private string[] words = [
// index from start index from end
"first", // 0 ^10
"second", // 1 ^9
"third", // 2 ^8
"fourth", // 3 ^7
"fifth", // 4 ^6
"sixth", // 5 ^5
"seventh", // 6 ^4
"eighth", // 7 ^3
"ninth", // 8 ^2
"tenth" // 9 ^1
]; // 10 (or words.Length) ^0
可以使用^1索引来检索最后一个单词。 在初始化下面添加以下代码:
Console.WriteLine($"The last word is < {words[^1]} >."); // The last word is < tenth >.
范围指定范围的 开始 和 结束 。 范围的开始是非独占的,但范围的末尾是独占的,这意味着 开始 包含在范围中,但 结束 不包括在范围中。
[0..^0] 范围表示整个范围,正如 [0..sequence.Length] 也表示整个范围。
以下代码使用单词“second”、“third”和“fourth”创建子范围。 它包括从 words[1] 到 words[3]。 该元素 words[4] 不在范围内。
string[] secondThirdFourth = words[1..4]; // contains "second", "third" and "fourth"
// < second >< third >< fourth >
foreach (var word in secondThirdFourth)
Console.Write($"< {word} >");
Console.WriteLine();
以下代码返回具有“第九”和“第十”的范围。 它包括 words[^2] 和 words[^1]。 结束索引 words[^0] 不被包括在内。
string[] lastTwo = words[^2..^0]; // contains "ninth" and "tenth"
// < ninth >< tenth >
foreach (var word in lastTwo)
Console.Write($"< {word} >");
Console.WriteLine();
以下示例创建具有开放起点、终点或两者都有的范围:
string[] allWords = words[..]; // contains "first" through "tenth".
string[] firstPhrase = words[..4]; // contains "first" through "fourth"
string[] lastPhrase = words[6..]; // contains "seventh", "eight", "ninth" and "tenth"
// < first >< second >< third >< fourth >< fifth >< sixth >< seventh >< eighth >< ninth >< tenth >
foreach (var word in allWords)
Console.Write($"< {word} >");
Console.WriteLine();
// < first >< second >< third >< fourth >
foreach (var word in firstPhrase)
Console.Write($"< {word} >");
Console.WriteLine();
// < seventh >< eighth >< ninth >< tenth >
foreach (var word in lastPhrase)
Console.Write($"< {word} >");
Console.WriteLine();
还可以将范围或索引声明为变量。 然后,可以在[]字符中使用变量。
Index thirdFromEnd = ^3;
Console.WriteLine($"< {words[thirdFromEnd]} > "); // < eighth >
Range phrase = 1..4;
string[] text = words[phrase];
// < second >< third >< fourth >
foreach (var word in text)
Console.Write($"< {word} >");
Console.WriteLine();
下面的示例显示了这些选择的许多原因。 修改x和yz尝试不同的组合。 在试验时,请使用x小于y且y小于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]}");
不仅数组支持索引和区间。 还可以将索引和范围与字符串Span<T>ReadOnlySpan<T>一起使用。
隐式范围运算符表达式转换
使用 range 运算符表达式语法时,编译器会隐式地将开始值和结束值转换为 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。
索引和范围的类型支持
索引和范围提供清晰简洁的语法,用于访问序列中的单个元素或一系列元素。 索引表达式通常返回序列元素的类型。 范围表达式通常返回与源序列相同的序列类型。
明确提供索引器带有Index或Range参数的任何类型分别显式支持索引或范围。 采用单个 Range 参数的索引器可能会返回不同的序列类型,例如 System.Span<T>。
重要
使用 range 运算符的代码性能取决于序列作数的类型。
范围运算符的时间复杂性取决于序列类型。 例如,如果序列是一个 string 或一个数组,则结果是输入的指定节的副本,因此时间复杂性为 O(N)( 其中 N 是范围的长度)。 另一方面,如果是System.Span<T>或System.Memory<T>,那么结果引用相同的后备存储,这意味着没有发生复制,并且操作是O(1)。
除了时间复杂性之外,这还会导致额外的分配和复制操作,从而影响性能。 在性能敏感代码中,请考虑使用 Span<T> 或 Memory<T> 用作序列类型,因为范围运算符不会为其分配。
类型如果有名为Length或Count的属性,并且这些属性具有可访问的getter及返回类型为int,则该类型被认为是可计数的。 不显式支持索引或范围的可计数类型可能会为它们提供隐式支持。 有关详细信息,请参阅功能建议说明的隐式索引支持和隐式范围支持部分。 使用隐式范围支持的范围返回与源序列相同的序列类型。
例如,以下 .NET 类型支持索引和范围: String, Span<T>以及 ReadOnlySpan<T>。 支持 List<T> 索引,但不支持范围。
Array 具有更细微的行为。 单个维度数组支持索引和范围。 多维数组不支持索引器或范围。 多维数组的索引器具有多个参数,而不是单个参数。 Jagged 数组(也称为数组数组)支持范围和索引器。 以下示例演示如何遍历交错数组的矩形区域。 它迭代中心部分,排除第一行和最后三行,以及每个选定行的第一列和最后两列。
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 的范围运算符用于分配数组以存储返回的元素。
索引和范围的应用场景
如果要分析较大序列的一部分,通常会使用范围和索引。 新语法在明确指出序列所涉及的部分时更加清晰。 本地函数 MovingAverage 采用 Range 其参数。 然后,该方法在计算最小值、最大值和平均值时仅枚举该范围。 在项目中尝试以下代码:
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