.NET Tools
Essential productivity kit for .NET and game developers
Indices, Ranges, and Null-coalescing Assignments – A Look at New language features in C# 8
“What are the new language features in C# 8? And is C# 8 supported in ReSharper and Rider?” Two very good questions!
With every new C# version that is released, we try to cover what’s new in the programming language we all use daily (C# 7, C# 7.1, 7.2 and 7.3). Today looks like a great day to start covering what’s new and coming in C# 8.
In case you want to start playing with C# 8, both ReSharper 2019.1 EAP and Rider 2019.1 EAP come with initial support for it.
In this post, we will cover indices, ranges and null-coalescing assignment. In future posts, we will look at switch expressions, patterns, async streams and IAsyncEnumerable
, nullable reference types and more. Keep an eye on our blog – we’ll span a whole range of C# 8!
In this series:
- Indices, Ranges, and Null-coalescing Assignments
- Switch Expressions and Pattern-Based Usings
- Recursive Pattern Matching
- Async Streams
- Nullable Reference Types: Migrating a Codebase
- Nullable Reference Types: Contexts and Attributes
How to get C# 8?
First of all, we will need the .NET Core 3.0 preview installed on our machine. For ReSharper, Visual Studio 2019 is also required.
Once installed, we can edit our project and update the language version to specify we want to make use of C# 8 (or preview
). In Rider, we can do this from the Project Properties dialog:
The manual way would be to edit the .csproj
file as follows:
<PropertyGroup> <LangVersion>preview</LangVersion> </PropertyGroup>
When using ReSharper or Rider, there is also a quick-fix that helps us set the language version with an Alt+Enter when our code contains new language features:
Indices and Ranges
Working with arrays becomes more expressive with the new types and operators introduced for indices and ranges (discussion). Previously, when we had to address items based on their position to the end of an array, the resulting code has looked rather clumsy:
var numbers = Enumerable.Range(1, 10).ToArray(); var lastItem = numbers[numbers.Length - 1];
Also, when we had to slice an array given the start and end position, there was quite some ceremony involved, for instance to calculate the length, pre-initialize an array or falling back to:
var (start, end) = (1, 7); var length = end - start; // Using LINQ var subset1 = numbers.Skip(start).Take(length); // Or using Array.Copy var subset2 = new int[length]; Array.Copy(numbers, start, subset2, 0, length);
The latter method using Array.Copy
is roughly what the compiler will emit behind the scenes when using the new Index
and Range
types.
But let’s focus on the more simple example of accessing an item from the end. This can be written as follows:
Pro tip: this quick-fix can be applied in bulk on the full solution.
Here, the hat operator ^
indicates that we’re referring to an item coming from the end. Similarly, the item before the last would be ^2
and so on. Want to take a guess what ^0
results in? Right, we would get an IndexOutOfRangeException
. Likewise, negative numbers would result in an ArgumentOutOfRangeException
. By the way, the ^1
expression is of the type Index
and could easily be extracted into a dedicated variable. We can also create them programmatically, for instance by calling new Index(1, fromEnd: true)
. Also, instances of int will implicitly convert to an Index
.
Let’s get back to the example of slicing an array. For this, we can make use of the range operator ..
:
void Print(Range range) => Console.WriteLine($"{range} => {string.Join(", ", numbers[range])}"); Print(start..3); // 1..3 => 2, 3 Print(..3); // 0..3 => 1, 2, 3 Print(3..); // 3..^0 => 4, 5, 6, 7, 8, 9, 10 Print(1..^1); // 1..^1 => 2, 3, 4, 5, 6, 7, 8, 9 Print(^2..^1); // ^2..^1 => 9
From this we can see that the end always denotes an exclusive index. Open range bounds will automatically have their start defined as 0
or their end as ^0
. A range that is open on both sides is synonymous for Range.All
(or 0..^0
) and results in referencing the array from its very first to last element. ReSharper and Rider will inform about redundant range bounds and provide a quick-fix:
For the IL-curious folks among us, it’s also worth taking a look at the generated IL code for indices and ranges. Instances of and Index
and Range
can freely be passed as arguments or variables, however, when defining ranges with literals, ReSharper and Rider will also check for a possible OverflowException
:
To avoid unnecessary copying and allocations, we might want to call array.AsSpan()
before slicing our array with a range, as this will access the original array. Also, many of the existing framework APIs, like Substring
or Slice
, have been updated to work with Index
and Range
objects.
Null-Coalescing Assignment
The new null-coalescing assignment (discussion) is probably the most straightforward addition to the new C#8 language features. All it does is to simplify the common task of assigning a value if the field, property or variable in question is null
. Previously, we could either use an if
-statement with null
-check or make use of the null-coalescing operator:
if (obj.Keys == null) obj.Keys = new List<string>(); obj.Keys = obj.Keys ?? new List<string>();
With the added C# 8 support, ReSharper and Rider will tell us how our code can be written more elegantly:
The To Compound Assignment context action also works with other relevant operators as with +
, -
, *
, /
, and more.
Download ReSharper Ultimate 2019.1 EAP or check out Rider 2019.1 EAP to start taking advantage of ReSharper’s language support updates and C# 8. We’d love to hear your thoughts!