The C# of today has progressed in exciting ways, and for the benefit of the development community. C# 8 is one of those releases where the sum of its parts add up to something great. In this post, we’ll be exploring the new Range and Index classes, and the syntax additions to the C# language that make them first-class citizens.

Range Class

The Range class is a data structure with two significant properties: Start and End. These properties are instances of the Index class, which we’ll discuss later. Here is a small excerpt from the class definition.

public readonly struct Range : IEquatable<Range>
{
    /// <summary>Represent the inclusive start index of the Range.</summary>
    public Index Start { get; }

    /// <summary>Represent the exclusive end index of the Range.</summary>
    public Index End { get; }

    /// <summary>Construct a Range object using the start and end indexes.</summary>
    /// <param name="start">Represent the inclusive start index of the range.</param>
    /// <param name="end">Represent the exclusive end index of the range.</param>
    public Range(Index start, Index end)
    {
        Start = start;
        End = end;
    }
...

To utilize the Range class, we can use the constructor in a traditional C# fashion.

var oldSchool = new Range(1,2);

We can also use the new C# 8 syntax.

// var is Range
var range = 1..2;

Remember, Range is a type that represents a start and end. There are no values from a range until we apply it to a collection. Some folks may confuse the syntax for getting values between the integers of 1 and 2, and that is not the case here.

var result = numbers[range];

When we have a Range instance, we can inspect its values at any point.

var range = 1..4; // Range
Console.WriteLine($"range starts at {range.Start} and ends at {range.End}");

Index Class

We can’t talk about Range without addressing the Index class, which provides a range instance with its start and end values. Let’s look at the constructor of the Index class.

public Index(int value, bool fromEnd = false)
{
    if (value < 0)
    {
        ThrowHelper.ThrowValueArgumentOutOfRange_NeedNonNegNumException();
    }

    if (fromEnd)
        _value = ~value;
    else
        _value = value;
}

We notice two crucial characteristics of the Index class:

  • An Index value is a non-negative integer.
  • The value can be from the beginning or end of a sequence.

The Index class has two properties of Value and IsFromEnd, which allow us to understand the intent of the instance.

We can instantiate an Index instance like any other C# class.

var oldSchool = new Index(1, fromEnd: true)

We can also use the syntax additions in C# 8 to shorten our declaration.

var index = ^1;

We can get the last index of a collection with the following code.

var last = numbers[^1];

It’s important to understand that the index of ^0 is the length of any collection. Using it will result in an IndexOutOfRangeException.

try
{
    var nope = numbers[^0];
}
catch (IndexOutOfRangeException exception)
{
    Console.WriteLine($"^0 is the length of array, which is out of bounds.");
}

Code Sample

We can see how Range and Index work in a simple console application.

class Program
{
    static void Main(string[] args)
    {
        var numbers = Enumerable.Range(1, 10).ToArray();

        var threeToFour = numbers[3..4];
        var last = numbers[^1];

        Console.WriteLine($"3..4: {string.Join(",", threeToFour)}");
        Console.WriteLine($"last: {string.Join(",", last)}");

        try
        {
            var nope = numbers[^0];
        }
        catch (IndexOutOfRangeException exception)
        {
            Console.WriteLine($"^0 is the length of array, which is out of bounds.");
        }

        var range = 1..4; // Range
        Console.WriteLine($"range starts at {range.Start} and ends at {range.End}");

        var index = ^4;
        Console.WriteLine($"The index is {index.Value} and is from the {(index.IsFromEnd ? "end" : "start")}");
    }
}

We can see the results in our console.

3..4: 4
last: 10
^0 is the length of array, which is out of bounds.
range starts at 1 and ends at 4
The index is 4 and is from the end

Limitations

There are limitations to the Range and Index classes, best described in the Microsoft documentation.

the following .NET types support both indices and ranges: String, Span<T>, and ReadOnlySpan<T>. The List<T> supports indices but doesn’t support ranges. Microsoft

The inconsistent support for Index and Range may be frustrating to users, and we may need additional calls to ToArray to make the newer C# 8 syntax work. We may inadvertently cause unnecessary runtime allocations trying to improve our development time experience.

If we are creating custom types, we need to consider what it takes to support both Range and Index.

The considerations for Range include:

  • The type is countable.
  • The type has an accessible member named Slice, which has two parameters of type int.
  • The type does not have an instance indexer, which takes a single Range as the first parameter. The Range must be the only parameter, or the remaining parameters must be optional.

The requirements for Index include:

  • The type is countable.
  • The type has an accessible instance indexer, which takes a single int as the argument.
  • The type does not have an accessible instance indexer, which takes an Index as the first parameter. The Index must be the only parameter, or the remaining parameters must be optional.

The Future?

While the new Range and Index classes are welcome additions to the C# family, I still feel limited by their use cases.

Warning, everything below here is theoretical and will not work

Generic Indexes and Ranges

As developers, we know that ranges are rarely about integers. We have natural ranges that include dates, times, alphabets, and more. It would be nice to see more support for human-friendly ranges.

var year = new DateTime(2020, 1,1)...new DateTime(2020, 12, 31);
// characters kind of work
var characters = a..z;
Console.WriteLine($"range starts at {(char)characters.Start.Value} and ends at {(char)characters.End.Value}");

While we can bend the rules of Index to support types today, we hope to see more support in future versions of C#.

More Consistent Range Support

List<T> is likely the most widely used generic data structure in .NET, and not having Range support seems strange. The only thing missing seems to be an implementation of Slice from the List type.

I would like to see more support for data structures across the .NET Framework, leaning more towards analyzers warning folks they are about to do something potentially costly.

Compiler Errors

While each type can implement its version of Slice, it may make sense to create compile-time analyzers for Range classes. Take the following example.

var wrong = numbers[1..0];

We’ve created a Range that starts at 1 and ends at 0, causing an impossible overlap. Different Slice implementations may or may not handle this appropriately. Still, it would be a nice added benefit for developers to add annotations to Slice implementations that give the C# compiler an understanding if a Range defined with constants may be tragically fated.

Conclusion

The Range and Index types will make for cleaner codebases, but they currently are limited to integer-based ranges. They are powerful, but their inconsistent availability might leave some developers scratching their heads. The different implementations may also lead to developers unwittingly allocating more runtime memory to get some fun syntax during development time. Still a relatively new feature, we can expect additions and updates to the language that make it even more powerful.

You can read more about Range and Index at the official Microsoft Documentation.

I hope you give Range and Index a try and leave a comment if you find something exciting or frustrating. I’d love to hear from you.