C# 8 – Excelling at Indexes

November 06, 2020
Written by
Dustin Ewers
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
AJ Saulsberry
Contributor
Opinions expressed by Twilio contributors are their own

csharp-8-indexes.png

“There are only two hard problems in computer science: cache invalidation, naming things, and off-by-one errors.” — Riff on a joke by Phil Karlton

After this post, one of these problems will become a little easier for you to solve: you’re going to learn about indices and ranges. While nearly everyone programming in C# uses standard indexes on a regular basis, there are some new ways of accessing arrays and lists that can make your development life a little bit easier. In a few minutes you’ll be up to speed on this C# 8.0 feature and more prepared to take on the C# 9.0 release.

Review

One of the first things you learn as a beginning programmer in any language is how to access an array. Most languages use a 0-based index. Some languages use 1-based array indexes (and are wrong), but the basic syntax is similar. Here are a few examples from different languages:

C#

var favoriteShows = new string[]{"Firefly", "Cowboy Bebop", "Samurai Champloo"};
var show = favoriteShows[1]; // "Cowboy Bebop"

JavaScript

var favShows = ["Firefly", "Cowboy Bebop", "Samurai Champloo"]
var show = favShows[1]  // "Cowboy Bebop"

Python

favShows = ["Firefly", "Cowboy Bebop", "Samurai Champloo"]
show = favShows[1] # "Cowboy Bebop"

While the basic syntax is the same, each language does things a little differently. In C#, Arrays are reference types, just like the other built-in collection types. They inherit from the object class and have a variety of different built in properties and methods. The array index syntax in C# is enabled by a feature called an indexer. Because indexers are a standard C# feature, they can be added to any custom object and are present on many array-like types such as Lists and Spans.

You can see working examples of different array-like objects in C# in the following code block. To run this code, spin up a new console app in Visual Studio 2019 or Visual Studio Code, or you can copy the code into an online IDE. .NET Fiddle is excellent for playing with new features and trying out code snippets, but be sure to set the Compiler field to .NET Core 3.1.

using System;
using SystemCollectionsGeneric;
using SystemCollections;

namespace CSharpFeatures
{
    class Program
    {
        static void Main()
        {
            var favoriteShows = new string[] { "Firefly", "Cowboy Bebop", "Samurai Champloo" };
            ConsoleWriteLine(favoriteShows[2]); // "Samurai Champloo"

            var listOfFavoriteShows = new List<string> { "Firefly", "Cowboy Bebop", "Samurai Champloo" };
            ConsoleWriteLine(listOfFavoriteShows[0]); // "Firefly"

            var spanOfFavoriteShows = new Span<string>(favoriteShows, 0, 2);
            ConsoleWriteLine(spanOfFavoriteShows[1]); // "Cowboy Bebop"
        }
    }
}

The index from end operator

While C# is versatile, there’s still some stuff that, until now, was only available in other languages. For example, languages like Python and Ruby allow you to use negative array indexes to access arrays in reverse.

Python 

favShows = ["Firefly", "Cowboy Bebop", "Samurai Champloo"]
show = favShows[-1] # "Samurai Champloo"

C# doesn’t support this, but C# 8.0 introduced a new feature that gets you the same functionality. This new operator is called the index from end operator: ^. By adding a ^ before your array index value, C# will start at the end of the array and count backward to locate an element. ^1 refers to the last element in the array.

This functionality is supported by the new System.Index struct. This struct was introduced in .NET Core 3.0 to support the new index from end syntax. This means you can store indexes as variables in your code.

Here are a few examples for you to try out:

using System;
using System.Collections.Generic;
using System.Collections;

namespace CSharpFeatures
{
    class Program
    {
        static void Main()
        {
            var favoriteShows = new string[] { "Firefly", "Cowboy Bebop", "Samurai Champloo" };

            Console.WriteLine(favoriteShows[^1]); // "Samurai Champloo"
            Console.WriteLine(favoriteShows[^2]); // "Cowboy Bebop"

            Index idx = ^3; // You can declare an index as a variable
            Console.WriteLine(favoriteShows[idx]); // "Firefly"

            Console.WriteLine(favoriteShows[^4]); // Throws Exception: "Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array."
            Console.WriteLine(favoriteShows[^0]); // Throws Exception: "Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array."
        }
    }
}

It’s just like using a negative index in other languages, just with a little bit different syntax. While this feature may not seem revolutionary on it’s own, it combines well with the next topic, Ranges.

Ranges

Another feature that’s common in other languages — including Ruby, Kotlin, F#, and Perl — is the ability to declare a range variable. Beginning with C# 8, you have the ability to declare a range. The Range struct contains a start and end index and allows you to use a single statement to slice chunks out of an indexable data structure.

In addition to the struct, C# has also introduced a new operator to make it look like other languages. The C# range operator looks like this: .. and is used like this: 1..3. The first number is the start index, which is inclusive. If you pick 1, it’ll start with the array[1]. The second number is the end index. The end index is exclusive. For example, a range of 1..1 will return nothing.

Here are a few examples of the range operator in action:

using System;

namespace CSharpFeatures
{
    class Program
    {
        static void Main()
        {
            var favoriteShows = new string[] { "Firefly", "Cowboy Bebop", "Samurai Champloo" };

            Range rg = 1..2; // You can declare a range variable
            WriteArray(favoriteShows[rg]); // "Cowboy Bebop"
            WriteArray(favoriteShows[0..3]); // "Firefly", "Cowboy Bebop", "Samurai Champloo"

            // You can also use "from end" indexes with ranges
            WriteArray(favoriteShows[^2..^0]); // "Cowboy Bebop", "Samurai Champloo"
        }

        private static void WriteArray(string[] arr)
        {
            Array.ForEach(arr, x => Console.Write(x + " | "));
            Console.WriteLine(" ");
        }
    }
}

The Range struct gives you a terse way to slice out chunks of an array. You can use it anytime you need to do an array slice. The example below uses from end indexing and ranges to average random samples from a data set. The amount of easy-to-mess-up array index math is vastly diminished when using these new operators.

using System;
using System.Linq;

namespace CSharpFeatures
{
    class Program
    {
        static void Main()
        {
            var sampleSize = 10;
            var values = GenerateData();
            var sample = GetRandomSample(values, sampleSize);

            Console.WriteLine(sample.Average());
        }

        private static int[] GetRandomSample(int[] data, int sampleSize) {
            var rand = new Random();
            var last = rand.Next(sampleSize, data.Length);
            var first = last - sampleSize;

            return data[^last..^first];
        }

        private static int[] GenerateData()
        {
            var rand = new Random();
            return Enumerable.Range(0, 50).Select(x => rand.Next(30, 80)).ToArray();
        }
    }
}

The output will be a Double like 61.1. Your mileage will be an average of pseudorandom signed Int32 values between 30 and 80, inclusive. I’d be stoked if my crossover got that kind of mileage.

Gotchas

As you learned above, different objects in C# use indexers. Most of those objects support the new Range and Index options; some do not. For example, the List object will work with a from end index, but it will not work with a range.

When it comes to arrays, your garden variety single dimension arrays support ranges and indexes, but multidimensional arrays do not. However, jagged arrays (arrays of arrays) support both.

Fortunately, the compiler catches these errors, so you don’t have to wait until runtime to find out if you made a mistake. It would be nice if everything worked with everything, but if it was easy, someone would program a computer to do it.

Summary

One of the great things about being a .NET developer is that if you see something cool in another platform, it’s usually only a matter of time before it will show up in C#. From end indexing and ranges are both pieces of syntax found in other languages that have found their way into the .NET family.

The next time you need to do some advanced array access, try out these new ways of slicing and dicing your data. Then you can focus more of your time dealing with cache invalidation and naming things.

Additional resources

Check out the following resources to dive deeper into the topics discussed in this post:

Microsoft Docs – What’s new in C# 8 – Indices and ranges

Microsoft Docs – C# Tutorials – Type support for indices and ranges

Microsoft Docs – C# Programming Guide – Using indexers

Quotes on Design – Looking for more piquant and memorable quotations about software and graphic design? Check out this edifying, minimalist source.

Dustin Ewers is a software developer hailing from Southern Wisconsin. He helps people build better software. Dustin has been building software for over 10 years, specializing in Microsoft technologies. He is an active member of the technical community, speaking at user groups and conferences in and around Wisconsin. He writes about technology at https://www.dustinewers.com. Follow him on Twitter at @DustinJEwers.