NDepend

Improve your .NET code quality with NDepend

C# Index and Range Operators Explained

C#8 added the index ^ and range .. operators. In this post I am attempting to demystify both in the most comprehensive way.

The index operator ^

Let’s start with the index ^ operator:

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 the collection.

Just make sure to 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:

What’s behind the index ^ operator syntactic sugar?

Actually the C# compiler translates these operators to the structures System.Index and System.Range introduced with .NET Core 3.0. 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 nor .NET Core 1.0 … 2.2 projects.

Here is what the compiler does for the index syntax:

In simple cases the C# compiler doesn’t need the Index structure and can do the indexing-from-the-end work in IL.

CSharp Compiler Optimization with Index
CSharp Compiler Optimization with Index

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).

Support for collections other than array

The index syntax ^ works for all collection types that have both:

  • a Count or Length property,
  • and a single integer indexer [int].

Index and Range support for mainstream collections

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 peaky and also requires the collection type to present a int[] Slice(int start, int length) method. As a consequence the range operator is not working with any of these collections while we could have expected that it works with at least IList<T> and List<T>.

Adding a Slice() method to IList<T> and List<T>collections was not an option because in many situations it is a breaking changes. Here is a reddit discussion about attempting to paliate 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?

Support for collections other than array

Finally here is an example of a custom collection supporting both index and range syntax:

Conclusion

Personally I need to thoroughly understand a syntactic sugar before using it, hence this post.

This new syntax is great but a bit limited due to the fact that IList<T> and List<T> don’t support range.

My dad being an early programmer in the 70's, I have been fortunate to switch from playing with Lego, to program my own micro-games, when I was still a kid. Since then I never stop programming.

I graduated in Mathematics and Software engineering. After a decade of C++ programming and consultancy, I got interested in the brand new .NET platform in 2002. I had the chance to write the best-seller book (in French) on .NET and C#, published by O'Reilly and also did manage some academic and professional courses on the platform and C#.

Over my consulting years I built an expertise about the architecture, the evolution and the maintenance challenges of large & complex real-world applications. It seemed like the spaghetti & entangled monolithic legacy concerned every sufficiently large team. As a consequence, I got interested in static code analysis and started the project NDepend in 2004.

Nowadays NDepend is a full-fledged Independent Software Vendor (ISV). With more than 12.000 client companies, including many of the Fortune 500 ones, NDepend offers deeper insight and full control on their application to a wide range of professional users around the world.

I live with my wife and our twin kids Léna and Paul in the beautiful island of Mauritius in the Indian Ocean.

Comments:

  1. 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);

  2. 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

  3. 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 🙂

Comments are closed.