C# 8: Indexes and Ranges

A new feature with .NET Core 2.1 is the Span type. This type allows direct access to memory in the stack, in the managed heap, and in the native heap. A great feature of the Span type is to take a split of the memory to access only an area within. The next step is to extend this into the C# programming language with ranges. Using ranges you can directly access a range within an array, a string, or a Span.

Sushi selection

This is the third article with features on C# 8. Here are the other ones about nullable reference types, and extensions to pattern matching:

C# 8: Pattern Matching Extended

C# 8 & No More NullReferenceExceptions – What about legacy code?

Framework Requirements

Some C# features require specific types from the framework. For example, interpolated strings are based on the FormattableString class. The foreach statement makes use of IEnumerable, and IEnumerator interfaces. The using statement makes use of the IDisposable interface. These are just a few examples, and there are a lot more. The new C# 8 feature with indexes and ranges makes use of the Index struct, the Range struct, and extension methods for arrays, the Span type, and strings. All these types need to be defined in the System namespace.

The complete source code for the requirements for indexes and ranges be copied from the csharplang GitHub repository.

The Index struct is used to hold an index which not only allows accessing a range from the begin but also from the end. If the index is used from the end, the index is created passing fromEnd = true with the constructor. This way, the inner value is assigned is assigned a value using the bitwise complement operator. This struct defines the Value property that returns the index value, the FromEnd property that returns true if the index is used from the end, and an operator to convert an int value to the Index struct.

public readonly struct Index
{
private readonly int _value;
public int Value => _value < 0 ? ~_value : _value;
public bool FromEnd => _value < 0;
public Index(int value, bool fromEnd)
{
if (value < 0) throw new ArgumentException("Index must not be negative.", nameof(value));
_value = fromEnd ? ~value : value;
}
public static implicit operator Index(int value)
=> new Index(value, fromEnd: false);
}

view raw
Index.cs
hosted with ❤ by GitHub

The Range struct makes use of the Index. This struct is constructed by invoking static Create, FromStart, ToEnd, and All methods.

  • The Create method accepts two Index parameters to define the range. Remember, the Index type implements an operator to convert int to Index, so you can pass an int as well. The Create method makes use of the private constructor to fill the Start and End properties.
  • The FromStart method just needs one parameter to create a range from the specified start up to the end. `
  • The ToEnd method returns a range from the start to the end point that is referenced with the parameter of the ToEnd method.
  • Finally, the All method returns a range from the begin to the end.
public readonly struct Range
{
public Index Start { get; }
public Index End { get; }
private Range(Index start, Index end)
=> (Start, End) = (start, end);
public static Range Create(Index start, Index end) => new Range(start, end);
public static Range FromStart(Index start) => new Range(start, new Index(0, fromEnd: true));
public static Range ToEnd(Index end) => new Range(new Index(0, fromEnd: false), end);
public static Range All() => new Range(new Index(0, fromEnd: false), new Index(0, fromEnd: true));
}

view raw
Range.cs
hosted with ❤ by GitHub

Extension methods are implemented for an int array, a generic array, the string type, Span, a generic Span. The methods implemented are get_IndexerExtension, Slice, and Substring. Some of these methods make use of the Slice method offered by the Span type to create a range.

static class Extensions
{
public static int get_IndexerExtension(this int[] array, Index index) =>
index.FromEnd ? array[array.Length index.Value] : array[index.Value];
public static int get_IndexerExtension(this Span<int> span, Index index) =>
index.FromEnd ? span[span.Length index.Value] : span[index.Value];
public static char get_IndexerExtension(this string s, Index index) =>
index.FromEnd ? s[s.Length index.Value] : s[index.Value];
public static Span<int> get_IndexerExtension(this int[] array, Range range) =>
array.Slice(range);
public static Span<int> get_IndexerExtension(this Span<int> span, Range range) =>
span.Slice(range);
public static string get_IndexerExtension(this string s, Range range) =>
s.Substring(range);
public static Span<T> Slice<T>(this T[] array, Range range) =>
=> array.AsSpan().Slice(range);
public static Span<T> Slice<T>(this Span<T> span, Range range)
{
var (start, length) = GetStartAndLength(range, span.Length);
return span.Slice(start, length);
}
public static string Substring(this string s, Range range)
{
var (start, length) = GetStartAndLength(range, s.Length);
return s.Substring(start, length);
}
private static (int start, int length) GetStartAndLength(Range range, int entityLength)
{
var start = range.Start.FromEnd ? entityLength range.Start.Value : range.Start.Value;
var end = range.End.FromEnd ? entityLength range.End.Value : range.End.Value;
var length = end start;
return (start, length);
}
}

view raw
Extensions.cs
hosted with ❤ by GitHub

The Span type is covered in Chapter 17, Managed and Unmanaged Memory of my new book Professional C# 7 and .NET Core 2.0.

Now, let’s look at how these types are used from the new C# 8 syntax – after installing a preview for C# 8.

Install C# 8

At the time of this writing, C# 8 is not yet released. However, you can try it out. At the time of this writing, to try C# 8 ranges, Visual Studio 2017 15.5-15.7 is needed – and the preview of C# Patterns and Ranges. You can easily install it, and uninstall it as described in the referenced article.

Be aware that the features shown here might look different in the released product, or also might be removed or delayed to a later version of C#.

The Hat Operator

We have a new operator – the hat operator. What type do you expect the variable last to be? It’s Index, and references the last element of a range. Using ^, fromEnd is supplied with a true value in the constructor of the Index struct:

var last = ^1;

view raw
TheHatOperator.cs
hosted with ❤ by GitHub

This variable can now be used to access the last element of an array. The result shows 3. This is so much simpler compared to using arr.Length - 1 to access the last item:

var last = ^1;
int[] arr = { 1, 2, 3 };
int lastItem = arr[last];
Console.WriteLine(lastItem);
int lastItem2 = arr[arr.Length 1];
Console.WriteLine(lastItem2);

view raw
HatOperator2.cs
hosted with ❤ by GitHub

Using the hat operator indexed by 1 returns the last element of the collection, 2 returns the one before that, 3 the one before that, and so on.

Range

Let’s get into the new range syntax using a string. First, let’s use the traditional ‘Substring’ method of a string. First, the last two words of the string are accessed using the 37nd character (which is the ‘l’), and a string length of 9 characters – this returns the string lazy dogs. Counting the first character of the string from the end, the Length property of the string can be used:

string text1 = "the quick brown fox jumped over the lazy dogs";
string lazyDogs1 = text1.Substring(36, 9);
string lazyDogs2 = text1.Substring(text1.Length 9, 9);

view raw
Range1.cs
hosted with ❤ by GitHub

It’s a lot easier to use the range syntax. The range starts from the 9th-last character, and ends after the last. With the range you need to supply the position after the last item with the range end:

string lazyDogs3 = text1[^9..^0];

view raw
Range2.cs
hosted with ❤ by GitHub

Behind the scenes, this gets translated to create two variables of type Index, and to invoke the Range.Create method with returns a Range value. Next, the new extension method of the string, Substring is used to pass a Range:

var start = ^9;
var end = ^0;
var range = Range.Create(start, end);
string lazyDogs4 = text1.Substring(range);

view raw
Range3.cs
hosted with ❤ by GitHub

Instead of supplying the element after the last item, you can simple use [^9..] to access the latest nine elements. This is translated to an invocation of Range.FromStart. Similar, counting from the beginning, you can use [36..] to count from the 37nd character up to the end. Starting from the beginning up to the nth element, you can use [..9]. This translates to the method Range.ToEnd. Just supplying two dots [..] returns the complete collection with Range.All.

string lazyDogs5 = text1[^9..]; // Range.FromStart
string lazyDogs6 = text1[36..]; // Range.FromStart
string thequick = text1[..9]; // Range.ToEnd
string completeString = text1[..]; // Range.All

view raw
Range Syntax
hosted with ❤ by GitHub

Ranges with Arrays

Let’s use the range syntax with a simple array. Here, using the range [2..5] returns a Span containing the 3rd up to the 5th element. Applying the range on an array invokes the Slice method of the Range struct which in turn invokes the AsSpan method on the array, and the Slice method of the Span passing the start and length. Because the Span type directly references the memory from the array, you can use ref locals to create a variable directly referencing the element from the array. This allows changing of the array element. Not using the syntax for ref return copies the value into a local int.

var arr = new[] { 1, 4, 8, 11, 19, 31 };
var range = arr[2..5];
ref int elt = ref range[1];
elt = 42;
int copiedelement = range[1];
copiedelement = 11;
Console.WriteLine($"the original element is changed: {arr[3]}");

view raw
RangeWithArray.cs
hosted with ❤ by GitHub

ref locals are part of C# 7 and discussed in a previous blog post C# 7.0 Out Vars and Ref Returns.

Summary

C# 7 introduced ref locals and ref returns, as well as more enhancements on reference semantics. The Span type allows directly referencing memory on the heap and on the stack. The Span type makes it easy to create slices with the Slices method. With C# 8, the tour continuous, and we get C# syntax on using ranges, which can be used to create substrings as well.

What do you think about this new C# 8 feature?

Get the complete sample from More Samples!

If you found this information valuable and want to return me a favor, then buy me a coffee.

Buy Me A Coffee

Before C# 8 is released, read about all the cool C# 7 features in the book Professional C# 7 and .NET Core 2.0!

Enjoy programming and learning,
Christian

12 thoughts on “C# 8: Indexes and Ranges

    1. Thanks for the comment.
      You’re right – just the method name is good enough 🙂 I didn’t mention this in this article.
      With C# 7, the await keyword works with a GetAwaiter method as well and doesn’t require the Task type.

      Liked by 1 person

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.