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:
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]);
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 ^:
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..]);
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:
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..]));
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:
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);
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:
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));
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:
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);
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:
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; }
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.
Page 1: a, b, c Page 2: d, e, f Page 3: g, h Last item: h