Поделиться через


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

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

В этом руководстве описано, как:

  • Используйте синтаксис при работе с интервалами в последовательностях.
  • Неявно определяет объект Range.
  • Изучите решения по проектированию для начала и окончания каждой последовательности.
  • Узнайте о сценариях для типов Index и 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] охватывает весь диапазон.

Следующий код создает подранг со словами "второй", "третий" и "четвертый". Он включает себя с 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, 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'

Это важно

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

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

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

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

Это важно

Производительность кода с помощью оператора диапазона зависит от типа операнда последовательности.

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

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

Тип считается считаемым, если у него есть свойство с именем 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();
}

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

См. также