In this post, we comprehensively demystify the C# index ^
and range ..
operators.
The index operator ^
Let’s start with the index ^
operator:
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 |
// The array used for all demonstrations across this post var arr = new[] { 0, 1, 2, 3, 4, 5 }; Assert.IsTrue(arr.Length == 6); // Just to clarify index and arr[index] are equals for (var i = 0; i < arr.Length; i++) { Assert.IsTrue(arr[i] == i); } // [^1] means last element // equivalent to [arr.Length - 1] Assert.IsTrue(arr[^1] == 5); Assert.IsTrue(arr[arr.Length - 1] == 5); // [^2] means second last element // equivalent to [arr.Length - 2] and so on Assert.IsTrue(arr[^2] == 4); for (var i = 0; i < arr.Length; i++) { // arr[^i] index from the end reads: arr[arr.Length -i] Assert.IsTrue(arr[^(arr.Length - i)] == i); Assert.IsTrue(arr[^(6 - i)] == i); // A little headache... but it makes sense! Assert.IsTrue(arr[^(i + 1)] == 5 - i); } // arr[^0] means index after last element arr[arr.Length] // and throw a IndexOutOfRangeException bool exThrown = false; try { int i = arr[^0]; } catch (IndexOutOfRangeException) { exThrown = true; } Assert.IsTrue(exThrown); |
If you are used to regular expression (regex) this syntax is a bit misleading. In C# the ^
operator means index-from-the-end while in regex the character ^
matches the starting position within the string.
The range operator ..
The range operator ..
is used to make a slice of a collection.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// [..] means range of all elements Assert.IsTrue(arr[..].SequenceEqual(arr)); // range [1..4] returns {1, 2, 3 } // start of the range (1) is inclusive // end of the range (4) is exclusive Assert.IsTrue(arr[1..4].SequenceEqual(new [] { 1 , 2 , 3 })); // [..3] returns { 0, 1, 2 } from the beginning till 3 exclusive Assert.IsTrue(arr[..3].SequenceEqual(new [] { 0, 1, 2 })); // [3..] returns { 3, 4, 5 } from 3 inclusive till the end Assert.IsTrue(arr[3..].SequenceEqual(new [] { 3, 4, 5 })); |
Keep in mind that:
- start of the range is inclusive
- end of the range is exclusive
If you’ve been studied mathematic this syntax is a bit misleading. The C# syntax [1..4]
translates to this math notation[1..4[
.
Mixing index and range operators
Both operators can be mixed within the same expression:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// [0..^0] means from the beginning till the end // It is equivalent to [..] // Remember that the upper bound ^0 is exclusive // so there is no risk of IndexOutOfRangeException here Assert.IsTrue(arr[0..^0].SequenceEqual(arr)); // [2..^2] means [2..(6-2)] means [2..4] Assert.IsTrue(arr[2..^2].SequenceEqual(new[] { 2, 3 })); // [^4..^1] means [(6-4)..(6-1)] means [2..5] Assert.IsTrue(arr[^4..^1].SequenceEqual(new[] { 2, 3, 4 })); |
What’s behind the index ^ operator syntactic sugar?
Actually, the C# compiler translates these operators to the structures System.Index and System.Range. These structures were introduced with .NET Core 3.0 in 2019. These structures are also provided by .NET Standard 2.1 but not .NET Standard 2.0. It means that you cannot use this syntax within your .NET Framework projects.
Here is what the compiler does for the index syntax:
1 2 3 4 5 6 7 8 9 |
Index lastIndex = ^1; // translated to lastIndex = new Index(1, true); // true means fromEnd: true Assert.IsTrue(arr[^1] == 5); // translated to Assert.IsTrue(arr[lastIndex] == 5); // translated to Assert.IsTrue(arr[lastIndex.GetOffset(arr.Length)] == 5); |
In simple situations, the C# compiler doesn’t need the Index
structure and can do the indexing-from-the-end work in IL.
What’s behind the range .. operator syntactic sugar?
We already mentioned the structure Range. Here is how it is used by the compiler. Notice the call to the special method T[] System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray<T>(T[] array, Range range).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Range range = 2..^1; // translated to range = new Range(2, ^1); // translated to (note that the literal 2 is translated to new Index(2)) range = new Range(new Index(2), new Index(1, true)); Assert.IsTrue(arr[2..^1].SequenceEqual(new[] { 2, 3, 4 })); // translates to Assert.IsTrue(arr[range].SequenceEqual(new[] { 2, 3, 4 })); // translates to Assert.IsTrue(arr[new Range( new Index(2), new Index(1, true).GetOffset(arr.Length))] .SequenceEqual(new[] { 2, 3, 4 })); // translates to Assert.IsTrue( System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray( arr, new Range( new Index(2), new Index(1, true).GetOffset(arr.Length))) .SequenceEqual(new[] {2, 3, 4})); |
Support for collections other than array
The index syntax ^ works for all collection types that have both:
- a
Count
orLength
property, - and a single integer indexer
[int]
.
The code screenshot below shows what work and what doesn’t work.
As we can see the index syntax ^ works with IList<T>
and List<T>
but not with ISet<T>
, Hashset<T>
, IDictionary<K,V>
and Dictionary<K,V>
. Those last four are not indexed collections.
The range syntax .. is more restrictive and also requires the collection type to present an int[] Slice(int start, int length)
method. Because of this, the range operator is not working with any of these collections. Not even with IList<T>
and List<T>
 as one might have anticipated.
Adding a Slice()
method to IList<T>
and List<T>
collections was not an option because in many situations it is a breaking change. Here is a reddit discussion about attempting to palliate this lack. It looks like adding a Slice()
method to IList<T>
with the default interface implementation syntax could have been an option that would have avoided breaking changes. I didn’t find the reason why it hasn’t been done, do you know?
A custom collection that works with C# Index and Range Operators
Finally here is an example of a custom collection supporting both index and range syntax:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var myCollection = new MyCollection<int>(0, 1, 2, 3, 4, 5); Assert.IsTrue(myCollection[^1] == 5); Assert.IsTrue(myCollection[2..^1].SequenceEqual(new[] { 2, 3, 4 })); class MyCollection<T> { internal MyCollection(params T[] array) { _array = array; } private T[] _array; // Length or Count is required for both [^1] and [2..^1] internal int Length => _array.Length; // Int indexer is required for [^1] but not for [2..^1] internal T this[int index] => _array[index]; // Slice is required for [2..^1] but not for [^1] internal T[] Slice(int start, int length) { var slice = new T[length]; Array.Copy(_array, start, slice, 0, length); return slice; } } |
Conclusion
As a C# developer, I need to thoroughly understand syntactic sugar before using it, hence this post.
This new syntax is great but a bit limited because IList<T>
and List<T>
don’t support range.
What makes it confusing for me is that c# is 0-based, while the index operator works 1-based. I come from a language that is fully 1-based so, working with a 0-based language requires some extra brainpower. Why is the last element in an array:
Assert.IsTrue(arr[^1] == 5);
Instead of
Assert.IsTrue(arr[^0] == 5);
Thanks for this explanation,
I miss some examples that might be instructive about the scope of the operators.
1. can I use the ^ index with a variable ?
x = 3;
y = arr[x]; ==> y = 3
z = arr[^x]; ==> y = ?
2. does the range arr[3..^3] include or exclude element 3?
3. how will this go wrong arr[^4..3] or arr[^8] ==> IndexOutOfRangeException
4. and definitely wrong arr[^-2] ==> IndexOutOfRangeException ?
Regards,
Rob
Handy ref – thanks. One bug spotted; You refer to IList type, but the code shown actually creates a List.
This is actually a good example of why it is often a good idea to declare the types of your vars 🙂