Индексы и диапазоны

Диапазоны и индексы обеспечивают лаконичный синтаксис для доступа к отдельным элементам или диапазонам в последовательности.

Из этого руководства вы узнаете, как выполнять следующие задачи:

  • Использовать этот синтаксис для диапазонов в последовательности.
  • Неявно определяет объект Range.
  • Проектировать начало и конец каждой последовательности.
  • Составлять сценарии для типов Index и Range.

Поддержка языков для индексов и диапазонов

Диапазоны и индексы обеспечивают лаконичный синтаксис для доступа к отдельным элементам или диапазонам в последовательности.

Поддержка языков опирается на два новых типа и два новых оператора:

  • System.Index представляет индекс в последовательности.
  • Индекс из конечного оператора ^, указывающий, что индекс относительно конца последовательности.
  • System.Range представляет вложенный диапазон последовательности.
  • Оператор ..диапазона, указывающий начало и конец диапазона в качестве операндов.

Начнем с правил для использования в индексах. Рассмотрим массив sequence. Индекс 0 совпадает с sequence[0]. Индекс ^0 совпадает с sequence[sequence.Length]. Выражение sequence[^0] создает исключение так же, как и sequence[sequence.Length]. Для любого числа n индекс ^n совпадает с sequence.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();

Следующий пример демонстрирует многие возможные причины для таких решений. Измените x, y и z и опробуйте различные комбинации. Поэкспериментируйте со значениями, у которых 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>).

Преобразования выражений операторов неявного диапазона

При использовании синтаксиса выражения оператора диапазона компилятор неявно преобразует начальные и конечные значения в и 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'

Важно!

Неявные преобразования, отбрасываемые в Int32Index случае отрицательного ArgumentOutOfRangeException значения. Аналогичным образом Index конструктор создает ArgumentOutOfRangeException исключение при отрицательном значении value параметра.

Поддержка типов для индексов и диапазонов

Индексы и диапазоны предоставляют четкий и краткий синтаксис для доступа к одному элементу или диапазону элементов последовательности. Выражение индекса обычно возвращает тип элементов последовательности. Выражение диапазона обычно возвращает тот же тип последовательности, что и исходная последовательность.

Любой тип, который предоставляет индексатор с параметром Index или Range, явно поддерживает индексы или диапазоны соответственно. Индексатор, который принимает один параметр Range, может возвращать другой тип последовательности, например System.Span<T>.

Важно!

Производительность кода, использующего оператор range, зависит от типа операнда последовательности.

Временная сложность оператора range зависит от типа последовательности. Например, если последовательность является string или массивом, то результатом будет копия указанного раздела входных данных, поэтому временная сложность имеет значение O(N) (где N — это длина диапазона). С другой стороны, если это System.Span<T> или System.Memory<T>, результат ссылается на то же резервное хранилище, что означает отсутствие копии и использование операции O(1).

Помимо временной сложности, создаются дополнительные выделения и копии, влияющие на производительность. В коде, чувствительном к производительности, рекомендуется использовать Span<T> или Memory<T> в качестве типа последовательности, так как оператор range не предоставляет им выделений.

Тип является счетным, если у него есть свойство с именем Length или Count с доступным методом получения и типом возвращаемого значения int. Счетный тип, который не поддерживает индексы или диапазоны явным образом, может предоставить неявную поддержку для них. Дополнительные сведения см. в разделах о поддержке неявного индекса и неявного диапазона в примечании к предлагаемой функции. Диапазоны с поддержкой неявных диапазонов возвращают тот же тип последовательности, что и исходная.

Например, следующие типы .NET поддерживают как индексы, так и диапазоны: String, Span<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();
}

Во всех случаях оператор range для 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

См. также