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>
, andReadOnlySpan<T>
. TheList<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. TheIndex
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.