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.
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
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 twoIndex
parameters to define the range. Remember, theIndex
type implements an operator to convertint
toIndex
, so you can pass anint
as well. TheCreate
method makes use of the private constructor to fill theStart
andEnd
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var last = ^1; |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var last = ^1; | |
int[] arr = { 1, 2, 3 }; | |
int lastItem = arr[last]; | |
Console.WriteLine(lastItem); | |
int lastItem2 = arr[arr.Length – 1]; | |
Console.WriteLine(lastItem2); |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
string lazyDogs3 = text1[^9..^0]; |
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
:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var start = ^9; | |
var end = ^0; | |
var range = Range.Create(start, end); | |
string lazyDogs4 = text1.Substring(range); |
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
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
string lazyDogs5 = text1[^9..]; // Range.FromStart | |
string lazyDogs6 = text1[36..]; // Range.FromStart | |
string thequick = text1[..9]; // Range.ToEnd | |
string completeString = text1[..]; // Range.All |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]}"); |
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.
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
I’m liking the range and hat operator syntax, looks a nice short cut for strings and arrays.
LikeLiked by 1 person
Thanks for the article! One small unrelated detail. foreach can use IEnumerable and IEnumerator but it’s not mandatory: https://blogs.msdn.microsoft.com/kcwalina/2007/07/18/duck-notation/
LikeLiked by 1 person
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.
LikeLiked by 1 person
Quo vadis C#
LikeLike
What do you think? I think it’s a good direction. With previous updates I could reduce the code I need to write. With upcoming versions this is similar.
Christian
LikeLike