C# 9.0 New Features
.NET 5.0 release candidate 1 (rc.1) was published on 2020-09-14, which marks another big step towards the official .NET 5.0 release. As part of 5.0, C# 9.0 will be released with a bunch of new features. This article summarizes some of the new features with examples.
Install .NET 5.0 SDK
Download .NET 5.0 SDK from this link: SDK 5.0.100-rc.1.
Verify the installation using the following command line:
dotnet --list-sdks 2.0.0 [C:\Program Files\dotnet\sdk] 2.1.202 [C:\Program Files\dotnet\sdk] 2.1.400 [C:\Program Files\dotnet\sdk] 3.0.100 [C:\Program Files\dotnet\sdk] 3.1.101 [C:\Program Files\dotnet\sdk] 3.1.200 [C:\Program Files\dotnet\sdk] 3.1.400 [C:\Program Files\dotnet\sdk] 3.1.402 [C:\Program Files\dotnet\sdk] 5.0.100-rc.1.20452.10 [C:\Program Files\dotnet\sdk]Check the current version in use:
dotnet --version 5.0.100-rc.1.20452.10
Now let's start to create a new project to explore these C# new features.
Create a dotnet console project
Use the following commands to create a Console application using .NET SDK 5.0 SDK.
mkdir csharp9 cd csharp9 dotnet new console
There are two files created:
Program.cs csharp9.csproj
Run the following command to compile and execute the application:
dotnet build dotnet run
The output looks like this:
Hello World!
Now open this project in Visual Studio Code to code easily:
code .
The project looks like the following screenshot in VS Code:
Now let's start to explore these new features in C# 9.0.
Init-only setters
Before version 9.0, C# provides very easy syntax to create properties.
The following code snippet creates a class with two properties:
public class MyClass { public MyClass(string a, int b) => (A, B) = (a, b); public string A { get; set; } public int B { get; private set; } }
The code is already very simple with getters and setters. To use this class, we can change the main function in Program.cs to the following:
class Program { static void Main(string[] args) { var myClass = new MyClass("Hello, C# ", 9); Console.WriteLine($"{myClass.A}{myClass.B}"); } }
This program outputs the following:
Hello, C# 9
From very beginning when C# was created, most of types are created as reference types. If you favor programming with immutable, the setters in the above example can be a problem as the value of property A can be changed after initialization:
myClass.A = "Hi, C# ";
The new init-only setter comes to rescue and you can define the class with init settings:
public class MyClass { public string A { get; init; } public int B { get; init; } }
And then the class can be instantiated:
var myClass = new MyClass { A = "Hello, C# ", B = 9 };
A compiler error will be raised if assignment is done to property A once the object is initialized:
error CS8852: Init-only property or indexer 'MyClass.A' can only be assigned insigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
Init accessors and read-only fields
This new init accessor can also be allowed to mutate the read-only fields of enclosing class:
public class MyClass2 { private readonly string a; private readonly int b; public string A { get => a; init => a = value ?? throw new ArgumentNullException(nameof(A)); } public int B { get => b; init => b = value; } }
You can mutate the values of A and B in object initializer:
var myClass2 = new MyClass { A = "Hello, C# ", B = 9 }; Console.WriteLine($"{myClass2.A}{myClass2.B}");
Record types
From C# 9.0, record types are introduced. It is a reference type that provides synthesized methods to provide value semantics for equality. Records are immutable by default. Immutable variables are commonly used in distributed big data computing frameworks. It can help reduce errors when working with data concurrently.
Define a record type
Record type can be defined easily using keyword record. In earlier preview versions, the keyword was data.
public record Customer { public string LastName { get; } public string FirstName { get; } public Customer(string first, string last) => (FirstName, LastName) = (first, last); }
You can also add init setters but it is not mandatory as the compiler will do that by default.
To use the defined record type, you can just easily initialize the object as you would do with normal reference types:
var cust = new Customer(first: "Raymond", last: "Tang");
Roslyn compiler automatically synthesizes several other methods:
- Methods for value-based equality comparisons
- Override for GetHashCode()
- Copy and Clone members
- PrintMembers and ToString()
- Deconstruct method
Positional records
There is a new very concise syntax to define a record type called positional records. The previous Customer record can be defined using the following code:
public record Customer(string FirstName, string LastName); var cust = new Customer("Raymond", "Tang");
Deconstruct method
With positional record types, data can be easily deconstructed via the Deconstruct method:
var cust = new Customer("Raymond", "Tang"); var (first, last) = cust; Console.WriteLine($"{first} {last}");
In the above code snippet, data is deconstructed to two variables named first and last. A with-expression instructs the compiler to create a copy of a record, but with specified properties modified:
var cust2 = cust with { FirstName = "Ray" };
The above code snippet creates another object by copying from cust variable with FirstName changed to 'Ray'. Object initialization syntax is used together with with-expression to change values.
Inheritances
Since record type itself is a reference type, inheritance is also allowed.
public record RetailCustomer(string FirstName, string LastName, int RetailAccountsCount) : Customer(FirstName, LastName);
The above code snippet creates a new record type named RetailCustomer with one more property named RetailAccountsCount.
Equals
Record types comparison are based on values:
var cust1 = new Customer("Raymond", "Tang"); var cust2 = cust1 with { FirstName = "Ray" }; var cust3 = cust2 with { FirstName = "Raymond" }; Console.WriteLine(cust1.Equals(cust2)); Console.WriteLine(cust1.Equals(cust3));
The output will be 'False' and 'True'.
Top-level statements
I previously always like other scripting languages' easiness without the need to create a main entry function. Now from C# 9.0, we can also do this easily using top-level statements. The typical use cases are Jupyter Notebooks and Azure Functions. In one project, only one file can have top-level statements.
The 'Hello, world!' example can now be changed to just one line of code:
System.Console.WriteLine("Hello C# 9!");
Pattern matching improvements
As mentioned in the official documentation, there are several pattern matching improvements:
- Type patterns match a variable is a type
- Parenthesized patterns enforce or emphasize the precedence of pattern combinations
- Conjunctive and patterns require both patterns to match
- Disjunctive or patterns require either pattern to match
- Negated not patterns require that a pattern doesn’t match
- Relational patterns require the input be less than, greater than, less than or equal, or greater than or equal to a given constant.
The following are some examples:
using static System.Console; WriteLine("Hellow, C# 9!"); static bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z'; static bool IsLetterOrSeparator(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ','; WriteLine(IsLetter('0')); WriteLine(IsLetterOrSeparator('!')); dynamic text = null; if (text is null) WriteLine("text is null"); if (text == null) WriteLine("text is null");
One of the my favorites is 'is not' syntax:
using static System.Console; var cust = new Customer("Raymond", "Tang"); if (cust is Customer) WriteLine("Yes"); if (cust is not RetailCustomer) WriteLine("Yes"); record Customer(string FristName, string LastName); record RetailCustomer(string FristName, string LastName, int RetailAccountsCount) : Customer(FristName, LastName);
The output is 'Yes' for both if statements.
Covariant returns
C# 9.0 now allows an override method to returns a type that is more specific than the type defined in the base class. This is a feature I've been waiting for quite a long time.
using static System.Console; WriteLine("Hello, C# 9!"); var panda = new Panda(); abstract class Food { public string Name { get; set; } } class Bamboo : Food { public string Color { get; set; } } abstract class Animal { public abstract Food GetFood(); } class Panda : Animal { public override Bamboo GetFood() { throw new System.NotImplementedException(); } }
Class Panda overrides GetFood method with returned type as Bamboo which inherits from class Food.
Other features
There are many other features that help us to write code very efficiently (let the complier do the work!).
One of the feature is that in a new expression the target type can be omitted if the created object type is already known:
using System.Collections.Generic; using static System.Console; // Traditional way List<int> numbers1 = new List<int>(); // The new concise approach List<int> numbers2 = new();
It can be combined with init-only properties to initialize values:
Customer cust = new() { FirstName = "Raymond" }; System.Console.WriteLine(cust.FirstName); record Customer { public string LastName { get; init; } public string FirstName { get; init; } }
Customer cust = new("Raymond", "Tang"); record Customer(string FirstName, string LastName);
Refer to the articles on the reference sections for more details.
References
- Language Feature Status
- What's new in C# 9.0
- Welcome to C# 9.0
- Machine Learning with .NET in Jupyter Notebooks
- .NET for Apache Spark Preview with Example