Last week Microsoft announced the official availability of C# 8.0 as part of the .NET Core 3.0 release, simultaneously at .NET Conf 2019 and on their development blog. The new language features include nullable reference types, asynchronous streams, default interface members, and new code patterns. All new features are supported in Visual Studio 2019.
Nullable reference types is one of the most important features of the new release. It is intended to prevent situations related to null reference exceptions through the use of specific syntax rules: the developer must explicitly express whether a certain variable may assume a null
value. In this case, the type name in the variable declaration must be appended with ?
(similarly to nullable value types):
string? foo;
If the variable does not have the ?
appended to the type name, it is considered as a non-nullable reference type. In this case, the compiler will enforce non-nullable rules: the variable must be initialized to a non-null value, and the variable can never be assigned the value null. It is possible (albeit not recommendable in most cases) to override this behavior with the null-forgiving operator !
following a variable name:
foo!.Length;
It is also possible to use nullable contexts to control if nullable warnings are given, or if nullable annotations have an effect. Nullable contexts can be specified either at the project level or within the source code file with the #nullable
and #pragma warning
pre-processor directives. Also, a type can have one of four nullabilities: Oblivious, nonnullable, nullable, and unknown. Different nullabilities will trigger different compiler behaviors. The complete specification for nullable reference types can be found here.
Another important feature is the introduction of asynchronous streams. The objective of the new feature is to introduce support for methods that are both iterators and asynchronous. Such methods can be used in scenarios where it is necessary to consume or produce continuous streams of results (i.e. from an IoT device, or a cloud service).
Async streams are implemented through the IAsyncEnumerable<T>
and IAsyncEnumerator<T>
interfaces, which can be used in conjunction with the async/await feature (originally introduced in C# 5.0). A method that returns an asynchronous stream must be declared with the async
modifier, and must also have one of the new interfaces as its return type. It must also contain yield return
statements to return successive elements in the asynchronous stream.
The following example is contained in Microsoft's official documentation. It implements a method that generates a sequence from 0 to 19, waiting 100 ms between generating each number:
public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
{
for (int i = 0; i < 20; i++)
{
await Task.Delay(100);
yield return i;
}
}
More details on asynchronous streams can be found in the official language specification.
Default interface members is a feature that allows the developer to add members to interfaces and provide an implementation for those members (thus allowing methods in interfaces with concrete implementations). The objective is to allow more flexibility for API authors, so they can add methods to an existing interface without breaking compatibility with past versions. This feature is similar to Java's Default Methods.
C# 8.0 also adds two new code patterns: recursive pattern matching and a pattern related to the using
statement. Recursive patterns, in a nutshell, allows patterns to contain other patterns - such as in the example below (also from Microsoft's official documentation):
IEnumerable<string> GetEnrollees()
{
foreach (var p in People)
{
if (p is Student { Graduated: false, Name: string name }) yield return name;
}
}
The pattern Student { Graduated: false, Name: string name }
checks that the Person
is a Student
, then applies the constant pattern false to their Graduated
property to see if they’re still enrolled, and the pattern string name to their Name
property to get their name (if non-null). Thus, if p
is a Student
, has not graduated and has a non-null name, we yield return that name.
The new pattern around the using
statement allows it to be added to a local variable declaration. In this case, the lifetime of the using
local will extend to the end of the scope in which it is declared. If there are more than one using
locals, they will be disposed in the reverse order in which they are declared:
{
using var foo1 = new FileStream("...");
using var foo2 = new FileStream("...");
...
// Dispose foo2
// Dispose foo1
}
Another capability added around the using
statement is the notion of a disposable pattern, i.e. a type that has an accessible Dispose instance method. Types following this pattern can participate in a using statement without having to implement IDisposable
:
class Bar
{
public void Dispose() { ... }
}
using (var foo = new Bar())
{
// statements
}
Other features added in C# 8.0 include changes in the switch statements syntax and the addition of target-typed new-expressions - which allows the omission of type declarations when creating new objects in contexts in which the type is already given:
Vector2[] vectors = { new (1, 1), new (2, -1) };
A summary of all new features can be found here, and the full specification proposal for C# 8.0 can be found here. Also, the technical sessions presented at .NET Conf 2019 highlighting the new language features can be found at YouTube (here and here). C# 8.0 is included in all Visual Studio 2019 versions.