Blog

C# 8: Indexes and Ranges

April 08, 2019 | 4 Minute Read

Index and ranges gives you another way to access elements in an array.


C# 8.0 Series

Want to read my other posts about C# 8?

Prerequisites & Setup

You will need Visual Studio 2019 and .NET Core 3.0 SDK to try out indexes and ranges. We need to modify the .csproj file to enable C# 8.0:

<LangVersion>8.0</LangVersion>

Let’s compare how we access specific elements in an array

Access elements is not something new. It’s fundamental to any programming language and thus also a concept in C#. Index is a new readonly struct added to the Framework.

I like to stick with cars in this post as well as I did in the previous C# 8 blog post. This time we have 10 parked cars that we get as an array:

    class Program
    {
        static void Main(string[] args)
        {
            Car[] parkedCars = GetAllParkedCars();

            // (1)
            var thirdCar = parkedCars[3];
            var lastCar = parkedCars[parkedCars.Length -1];

            // (2)
            var thirdCarLinq = parkedCars.ElementAt(3);
            var lastCarLinq = parkedCars.Last();

            // (3)
            var thirdCarByIndex = parkedCars[^7];
            var lastCarByIndex = parkedCars[^1];
        }

        private static Car[] GetAllParkedCars()
        {
            var l = new List<Car>();
            foreach (var item in Enumerable.Range(1,10))
            {
                l.Add(new Car($"Car {item}"));
            }
            return l.ToArray();
        }
    }

The first (1) is a common way how we access elements inside an array. LINQ (2) is the most natural to read but that comes also with a (minimal) cost - It’s slower than the other solutions because it iterates over the elements from the start until the condition is met. But think about that readability is usually in most cases more important than performance. The last (3) uses the new “hat”-operator to express the new index type:

  • [^7] means from the end count seven elements backwards.
  • [^1] means from the end the first element.

Pay attention that [^0] is not valid and will throw an exception - because [^0] is equal to [array.Length - 0] and this throws an IndexOutOfRangeException. The [^7] and [^1] are shorthand versions of this:

        // index as type
        var thirdCarByIndex2 = parkedCars[new Index(7, fromEnd: true)];
        var lastCarByIndex2 = parkedCars[new Index(1, fromEnd: true)];

Usually I want also understand how the compiler is emitting my C# code and how the .NET Runtime is handling it. You maybe wonder if [^1] is compiled to [array.Length - 1]. It is almost the same. Decompiling the assembly with IL-Spy proves this:

	index = new Index(1, true);
	Car car8 = array[index.get_IsFromEnd() ? (array.Length - index.get_Value()) : index.get_Value()];

The critical IL-Operation is the same as well:

By int:

array-length-sub-int

By Index:

array-length-sub-index

Access a range of specific elements

Let’s combine two indexes to form a range - makes sense, right?

Range is a readonly struct and a new type. It consists mainly of a Start and End property of type Index. The following code is usually what we do to get a range out of a string using Substring():

    var greeting = "Hello my name is Code Therapist!";
    var helloMyNameIs = greeting.Substring(0, 17);
    var codeTherapist = greeting.Substring(17, 9);

With ranges you can express exactly the same in a slightly different way:

    var helloMyNameIs = greeting[..17];
    var codeTherapist = greeting[17..^1];

It compiles internally to a call to Substring().

Closing words

I’m curious how this feature will change the way I or other developers write code in C#. In my opinion it’s very convenient in some situations - especially the Range. But compared to other features it’s not something completely new that you couldn’t do with a lower C# version. Simply because these two operators will be lowered to regular indexer/method calls.