Indices and ranges

Ranges and indices provide a succinct syntax for accessing single elements or ranges in an Array, Span<T>, or ReadOnlySpan<T>. These features enable more concise, clear syntax to access single elements or ranges of elements 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.
  • 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 represents an index into a sequence.
  • The ^ operator, which specifies that an index is relative to the end of a sequence.
  • 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. Consider an array sequence. The 0 index is the same as sequence[0]. The ^0 index is the same as sequence[sequence.Length]. Note that sequence[^0] does throw an exception, just as sequence[sequence.Length] does. 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

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 is not included in the range. The range [0..^0] represents the entire range, just as [0..sequence.Length] represents the entire range.

The following code creates a subrange with the words "quick", "brown", and "fox". It includes words[1] through words[3]. 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.

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

The following code creates a subrange with "lazy" and "dog". It includes words[^2] and words[^1]. The end index words[^0] isn't included. Add the following code as well:

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

The following examples create ranges that are open ended for the start, end, or both:

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

You can also declare ranges or indices as variables. The variable can then be used inside the [ and ] characters:

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

The following sample shows many of the reasons for those choices. Modify x, y, and z to try different combinations. 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:

var 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..];
Console.WriteLine($"\t{z_end[0]}..{z_end[^1]} is the same as {z_zero[0]}..{z_zero[^1]}");

Scenarios for Indices and Ranges

You'll often use ranges and indices when you want to perform some analysis on subrange of an entire sequence. The new syntax is clearer in reading exactly what subrange is involved. 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) =>

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

You can download the completed code from the dotnet/samples repository.