C# 8 in VS 2019 – Setting up the development environment
The long-awaited next major version of the C# language (C# 8.0) is nearing its final release.
It’s going to be released at the same time as .NET Core 3.0. This means that just like .NET Core 3.0 preview, C# 8 is also included in the Visual Studio 2019 versions.
Editorial Note: The final version of Visual Studio 2019 got released today, April 2nd, 2019. However NET Core 3.0 is still in its preview as of this writing. .NET Core 3.0 will not be released along with VS 2019, but sometime later in 2019.
If you want to try out all the already available new language features yourself, you also need to install the latest preview version of .NET Core 3.0. That’s because some of the language features depend on .NET types which will be a part of .NET Standard 2.1.
At the time of writing, the only .NET platform with these types is .NET Core 3.0 (Preview 2 or later). It’s also worth mentioning that there are currently no plans for a future .NET framework version to implement .NET Standard 2.1 and include the new types required for some of the C# 8 features.
To create a suitable project for trying out all currently available C# 8.0 features, you can follow these steps:
- Create a new .NET Core project of any type.
- In the Application pane of the project Properties window, make sure that the Target framework is set to .NET Core 3.0.
- From the Build pane of the project Properties window, open the Advanced Build Settings dialog and select C# 8.0 (beta) as the Language version.
Nullable reference types
Nullable reference types were already considered in the early stages of C# 7.0 development but were postponed until the next major version. The goal of this feature is to help developers avoid unhandled NullReferenceExceptionexceptions.
The core idea is to allow variable type definitions to specify whether they can have null value assigned to them or not:
IWeapon? canBeNull; IWeapon cantBeNull; |
Assigning a null value or a potential null value to a non-nullable variable results in a compiler warning (the developer can configure the build to fail in case of such warnings, to be extra safe):
canBeNull = null ; // no warning cantBeNull = null ; // warning cantBeNull = canBeNull; // warning |
Similarly, warnings are generated when dereferencing a nullable variable without checking it for null value first:
canBeNull.Repair(); // warning cantBeNull.Repair(); // no warning if (canBeNull != null ) { canBeNull.Repair(); // no warning } |
The problem with such a change is that it breaks existing code: the feature assumes that all variables from before the change are non-nullable. To cope with that, static analysis for null-safety can be enabled selectively with a compiler switch at the project level.
Developers can opt-in for nullability checking when they are ready to deal with the resulting warnings. Still, this should be in their own best interest, as the warnings might reveal potential bugs in their code.
The switch is persisted as a property in the project file. There’s no user interface in Visual Studio 2019 yet for changing its value. Therefore, the following line must be added manually to the first PropertyGroup element of the project file to enable the feature for the project:
<NullableContextOptions>enable</NullableContextOptions> |
For more granularity, the #pragma warning directives can be used to disable and re-enable individual warnings for a block of code. As an alternative, a new #nullable directive has been added. It can be used to enable support for nullable reference types for a block of code even if it is disabled at the project level:
#nullable enable IWeapon? canBeNull; IWeapon cantBeNull; canBeNull = null ; // no warning cantBeNull = null ; // warning cantBeNull = canBeNull; // warning #nullable restore |
It’s a good idea to use #nullable restore instead of #nullable disable to disable nullable reference types for the code that follows. This will ensure that the checks remain enabled for the rest of the file if you later decide to enable the feature for the whole project. Using #nullable disable would disable the checks even in that case.
Improvements to pattern matching
Some pattern matching features have already been added to C# in version 7.0.
Several new forms of pattern matching are being added to C# 8.0.
Tuple patterns
Tuple patterns allow matching of more than one value in a single pattern matching expression:
switch (state, transition) { case (State.Running, Transition.Suspend): state = State.Suspended; break ; case (State.Suspended, Transition.Resume): state = State.Running; break ; case (State.Suspended, Transition.Terminate): state = State.NotRunning; break ; case (State.NotRunning, Transition.Activate): state = State.Running; break ; default : throw new InvalidOperationException(); } |
Switch expression
The switch expression allows terser syntax than the switch statement when the only result of pattern matching is assigning a value to a single variable:
state = (state, transition) switch { (State.Running, Transition.Suspend) => State.Suspended, (State.Suspended, Transition.Resume) => State.Running, (State.Suspended, Transition.Terminate) => State.NotRunning, (State.NotRunning, Transition.Activate) => State.Running, _ => throw new InvalidOperationException() }; |
There are several differences in the syntax if we compare it to the switch statement:
- The left-hand variable of the assignment is specified only once before the expression, instead of in the body of each case.
- The switch keyword is placed after the tested value instead of placing it before it.
- The case keyword is not used anymore.
- The : character between the pattern and the body is replaced with a =>.
- Instead of break statements, the , character is used to separate the cases.
- For the body, expressions must be used instead of code blocks.
- For the catch-all case, a discard (_) is used instead of the default keyword.
A switch expression must always return a value. However, the code will still compile even if that’s not true (i.e. the cases do not cover all possible values):
state = (state, transition) switch { (State.Running, Transition.Suspend) => State.Suspended, (State.Suspended, Transition.Resume) => State.Running, (State.Suspended, Transition.Terminate) => State.NotRunning, (State.NotRunning, Transition.Activate) => State.Running }; |
The compiler will only emit a warning for the above code. If at runtime the tested value is not matched by any case, an InvalidOperationException will be thrown.
Positional patterns
When testing a type with a Deconstructor method, positional patterns can be used which have syntax very similar to tuple patterns:
if (sword is Sword(10, var durability)) { // code executes if Damage = 10 // durability has value of sword.Durability } |
The code assumes that the Sword type contains the following Deconstruct method:
public void Deconstruct( out int damage, out int durability) { damage = Damage; durability = Durability; } |
The pattern above compares the first deconstructed value and assigns the second deconstructed value to a newly declared variable. Although I’m using this pattern in an is expression, I could also use it in a switch expression or a switch statement.
Property patterns
Even if a type doesn’t have an appropriate Deconstruct method, property patterns can be used to achieve the same as with positional patterns:
if (sword is Sword { Damage: 10, Durability: var durability }) { // code executes if Damage = 10 // durability has value of sword.Durability } |
The syntax is a bit longer but also more expressive. It’s a better alternative to a positional pattern in cases when the order of values in the Deconstruct method would not be obvious.
Asynchronous streams
C# already has support for iterators and asynchronous methods. In C# 8.0, the two are combined into asynchronous streams. These are based on asynchronous versions of the IEnumerable and IEnumerator interfaces:
public interface IAsyncEnumerable< out T> { IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default ); } public interface IAsyncEnumerator< out T> : IAsyncDisposable { T Current { get ; } ValueTask< bool > MoveNextAsync(); } |
Additionally, an asynchronous version of the IDisposable interface is required for consuming the asynchronous iterators:
public interface IAsyncDisposable { ValueTask DisposeAsync(); } |
This allows the following code to be used for iterating over the items:
var asyncEnumerator = GetValuesAsync().GetAsyncEnumerator(); try { while (await asyncEnumerator.MoveNextAsync()) { var value = asyncEnumerator.Current; // process value } } finally { await asyncEnumerator.DisposeAsync(); } |
It’s very similar to the code we’re using for consuming regular synchronous iterators. However, it does not look familiar because we typically just use the foreach statement instead. An asynchronous version of the foreach statement is available for asynchronous iterators:
await foreach (var value in GetValuesAsync()) { // process value } |
Just like with the synchronous foreach statement, the compiler will generate the required code itself.
It is also possible to implement asynchronous iterators using the yield keyword, similar to how it can be done for synchronous iterators:
private async IAsyncEnumerable< int > GetValuesAsync() { for (var i = 0; i < 10; i++) { await Task.Delay(100); yield return i; } } |
You might have noticed the CancellationToken parameter of the GetAsyncEnumerator method of the IAsyncEnumerable<T> interface. As one would expect, it can be used to support cancellation of asynchronous streams.
However, there are currently no plans to support this parameter in compiler generated code. This means that you will need to write your own code for iterating over the items instead of using the asynchronous foreach statement if you want to pass the cancellation token to the GetAsyncEnumerator method.
Also, when implementing an asynchronous iterator with cancellation support, you will need to implement the IAsyncEnumerable<T> interface manually instead of using the yield keyword and relying on the compiler to do it for you.
Ranges and indices
C# 8.0 introduces new syntax for expressing a range of values.
Range range = 1..5; |
The starting index of a range is inclusive, and the ending index is exclusive. Alternatively, the ending can be specified as an offset from the end:
Range range = 1..^1; |
The new type can be used as an indexer for arrays. Both ranges specified above will give the same result when used with the following snippet of code:
var array = new [] { 0, 1, 2, 3, 4, 5 }; var subArray = array[range]; // = { 1, 2, 3, 4 } |
The new syntax can also be used to define:
An open-ended range from the beginning to a specific index:
var subArray = array[..^1]; // = { 0, 1, 2, 3, 4 } |
An open-ended range from a specific index to the end:
var subArray = array[1..]; // = { 1, 2, 3, 4, 5 } |
An index of a single item specified as an offset from the end.
var item = array[^1]; // = 5 |
The use of ranges and indices is not limited to arrays. They can also be used with the Span<T> type:
var array = new [] { 0, 1, 2, 3, 4, 5 }; var span = array.AsSpan(1, 4); // = { 1, 2, 3, 4 } var subSpan = span[1..^1]; // = { 2, 3 } |
Although that’s the full extent to which the Range type can be used with existing types in .NET Core 3.0 Preview 2, there are plans to provide overloads with Range-typed parameters for other methods as well, e.g.:
var subSpan = span.Slice(range); var substring = "range" [range]; |
So far, no clear information has been given if these will be a part of .NET Core 3.0 and .NET Standard 2.1. They could be added in a later version.
Using declaration
The using statement is a great way to ensure that the Dispose method will be called on a type implementing the IDisposable interface when an instance gets out of scope:
using (var reader = new StreamReader(filename)) { var contents = reader.ReadToEnd(); Console.WriteLine($ "Read {contents.Length} characters from file." ); } |
In C# 8.0, the using declaration is available as an alternative:
using var reader = new StreamReader(filename); var contents = reader.ReadToEnd(); Console.WriteLine($ "Read {contents.Length} characters from file." ); |
The using keyword can now be placed in front of a variable declaration. When such a variable falls out of scope (i.e. the containing block of code is exited) the Dispose method will automatically be called.
This can be especially useful when multiple instances of types implementing the IDisposable interface are used in the same block of code:
using var reader1 = new StreamReader(filename1); using var reader2 = XmlReader.Create(filename2); // process the files |
The above code is much more readable and less error-prone than the equivalent code written with the usingstatement:
using (var reader1 = new StreamReader(filename1)) using (var reader2 = XmlReader.Create(filename2)) { // process the files } |
Static local functions
Local functions were introduced in C# 7.0. They automatically capture the context of the enclosing scope to make any variables from the containing method available inside them:
public void MethodWithLocalFunction( int input) { Console.WriteLine($ "Inside MethodWithLocalFunction, input: {input}." ); LocalFunction(); void LocalFunction() { Console.WriteLine($ "Inside LocalFunction, input: {input}." ); } } |
In C# 8.0, you can declare a local function as static. This will prevent using the variables from the containing method in the local function and at the same time avoid the performance cost related to making them available. A variable from the containing method can of course still be passed to the local function as a parameter:
public void MethodWithStaticLocalFunction( int input) { Console.WriteLine($ "Inside MethodWithStaticLocalFunction, input: {input}." ); StaticLocalFunction(input); static void StaticLocalFunction( int input) { Console.WriteLine($ "Inside StatucLocalFunction, input: {input}." ); } } |
Disposable ref structs
C# 7.2 added support for structs which must be allocated on the stack (declared with the ref struct keywords). They are primarily useful in high-performance scenarios which require non-managed access to continuous blocks of memory (Span<T> is an example of such a type).
Such types are subject to many restrictions. Among others, they are not allowed to implement an interface. This also includes the IDisposable interface making it impossible to implement the disposable pattern.
Although they still can’t implement interfaces in C# 8.0, they can now implement the disposable pattern by simply defining the Dispose method:
ref struct RefStruct { // ... public void Dispose() { // release unmanaged resources } } |
This is enough to allow the type to be used with the using statement (or the using declaration):
using (var refStruct = new RefStruct()) { // use refStruct } |
Conclusion:
After a long anticipation, C# 8.0 is finally available in preview as part of Visual Studio 2019. Its final version will be released together with .NET Core 3.0.
Unlike all the versions of the language so far, not all features of C# 8.0 will be available in the .NET framework. Asynchronous streams and ranges depend on types which will only be added to .NET Core 3.0 and other .NET platforms implementing .NET Standard 2.1. As per the current plans, .NET framework will not be among them.
In my opinion, the most important new features of C# 8.0 are null reference types and improvements to pattern matching. I think so because they will help us write more reliable and readable code. Along with several smaller features, these will also be available in the .NET framework. This is a good enough reason to start using C# 8.0 even in .NET framework projects once it is released.