Indices, Ranges and Spans

C# has compact syntax for "the last element" (^1) and "elements 2 to 5" (2..5), plus collection expressions with a spread operator for building collections from others. Behind the scenes, Span<T> lets you work with a slice of an array or string without copying it — one of the reasons modern .NET is so fast.

Indexing from the end

^n means "n from the end": ^1 is the last element, ^2 the one before it. It works on arrays, strings, lists and spans:

C#
int[] values = [10, 20, 30, 40, 50];
List<string> names = ["Ana", "Ben", "Cy"];
string word = "elephant";

Console.WriteLine(values[^1]);
Console.WriteLine(values[^2]);
Console.WriteLine(names[^1]);
Console.WriteLine(word[^3]);
Output
50
40
Cy
a

values[^1] is simply a shorter, clearer way to write values[values.Length - 1]. Note that ^0 means "one past the end", so values[^0] throws.

Ranges

start..end selects a range. The start is included and the end is excluded; either can be left out, and either can use ^:

C#
int[] v = [0, 1, 2, 3, 4, 5, 6, 7];

Console.WriteLine(string.Join(" ", v[2..5]));    // 2, 3, 4
Console.WriteLine(string.Join(" ", v[..3]));     // first three
Console.WriteLine(string.Join(" ", v[^3..]));    // last three
Console.WriteLine(string.Join(" ", v[1..^1]));   // all but first and last
Console.WriteLine(string.Join(" ", v[..]));      // everything (a copy)

string file = "report-2026.pdf";
Console.WriteLine(file[..^4]);
Console.WriteLine(file[^3..]);
Output
2 3 4
0 1 2
5 6 7
1 2 3 4 5 6
0 1 2 3 4 5 6 7
report-2026
pdf

Indices and ranges are values too, with types Index and Range, so you can store them in variables:

C#
string[] months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
Range q3 = 6..9;
Index last = ^1;

Console.WriteLine(string.Join(", ", months[q3]));
Console.WriteLine(months[last]);

List<string> list = [.. months];
Console.WriteLine(string.Join(", ", list[9..]));
Output
Jul, Aug, Sep
Dec
Oct, Nov, Dec

Slicing an array, string or list with a range creates a new copy of those elements. For large data in hot code, a span avoids that copy (see below).

Collection expressions and spread

Collection expressions ([a, b, c]) work for arrays, lists, spans and other collection types — the target type decides what gets created. The spread element ..x inserts every element of another collection:

C#
int[] low = [1, 2, 3];
List<int> high = [8, 9];

int[] all = [0, .. low, 5, .. high];
List<int> asList = [.. all];
HashSet<int> unique = [.. low, .. low, 4];
int[] empty = [];

Console.WriteLine(string.Join(", ", all));
Console.WriteLine(asList.Count);
Console.WriteLine(string.Join(", ", unique));
Console.WriteLine(empty.Length);
Output
0, 1, 2, 3, 5, 8, 9
7
1, 2, 3, 4
0

You can also pass a collection expression straight to a method: Average([3, 4, 5]).

Span<T>: a view, not a copy

A Span<T> is a lightweight window onto a block of memory — part of an array, a string's characters, or memory on the stack. Slicing a span creates a new window over the same memory, so it's instant and changes are visible in the original:

C#
int[] data = [1, 2, 3, 4, 5, 6, 7, 8];

Span<int> middle = data.AsSpan(2, 4);    // elements 2..5, no copy
middle[0] = 99;
middle.Fill(0);
middle[^1] = 42;

Console.WriteLine(string.Join(", ", data));
Console.WriteLine(middle.Length);

Span<int> firstHalf = data.AsSpan()[..4];
firstHalf.Reverse();
Console.WriteLine(string.Join(", ", data));
Output
1, 2, 0, 0, 0, 42, 7, 8
4
0, 0, 2, 1, 0, 42, 7, 8

ReadOnlySpan<T> and strings

ReadOnlySpan<T> is a span you can't modify. Calling AsSpan() on a string gives a ReadOnlySpan<char>, which can be sliced and parsed without creating any substrings:

C#
string line = "2026-10-08;temperature;21.5";
ReadOnlySpan<char> span = line.AsSpan();

int first = span.IndexOf(';');
int last = span.LastIndexOf(';');

ReadOnlySpan<char> date = span[..first];
ReadOnlySpan<char> name = span[(first + 1)..last];
double value = double.Parse(span[(last + 1)..]);

Console.WriteLine(date.ToString());
Console.WriteLine(name.Length);
Console.WriteLine(value * 2);
Console.WriteLine(int.Parse(date[..4]) + 1);
Output
2026-10-08
11
43
2027

Parsing methods like int.Parse and double.Parse accept spans directly. Only ToString() copies characters into a new string.

Spans everywhere

Since C# 14, arrays (and strings, for read-only spans) convert to spans implicitly in more places, including when calling extension methods. Many library methods offer span-based overloads, and you can write your own methods that accept a span so callers can pass an array, part of an array, or stack memory:

C#
int[] numbers = [5, 10, 15, 20, 25];
Console.WriteLine(Sum(numbers));             // whole array
Console.WriteLine(Sum(numbers.AsSpan(1, 3))); // part of it
Console.WriteLine(Sum([1, 2, 3]));            // collection expression

Span<int> onStack = stackalloc int[3];        // memory on the stack, no garbage collection
onStack[0] = 7; onStack[1] = 8; onStack[2] = 9;
Console.WriteLine(Sum(onStack));

static int Sum(ReadOnlySpan<int> values)
{
    int total = 0;
    foreach (int v in values) total += v;
    return total;
}
Output
75
45
6
24

Span rules

Spans can point into the stack, so the compiler restricts where they can live: a span can't be a field of a class, can't be captured by a lambda, and can't be used across an await. When you need to store a slice, use Memory<T> or ArraySegment<T>. For most everyday code, plain arrays and ranges are fine — reach for spans when profiling shows that copying is a cost.

Try it yourself

Split a list into pages of three using ranges. Change pageSize and the number of items.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
Page 1: a, b, c
Page 2: d, e, f
Page 3: g, h
Last item: h