Table of Contents
In this post we’ll explore the new features introduced in C# 9. To use C# 9 in any of our projects, we need to install the .NET 5 preview version available for download on the Microsoft page.
We should specify in our .csproj file that it uses this particular version:
<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
And finally, mark the option Tools-> options ->environments ->preview features
. There you’ll find the option use previews of .NET Core SDK
and then restart Visual Studio.
1 - Init-only Properties
With init-only properties, we can create object initializers with immutable properties.
To do this, we create a class, in our example Car
, but instead of creating getters and setters or assigning values from the constructor, we do it like this:
public class Coche
{
public string Marca { get; init; }
public string Modelo { get; init; }
}
As you can see, we don’t have a setter
anymore. Instead, we have a new keyword called init
. This way, we can use it as object initializers in C# 9.
Coche coche1 = new Coche { Marca = "Opel", Modelo = "Vectra" };
Coche coche2 = new Coche { Marca = "Audi", Modelo = "A3" };
Console.WriteLine($"Mi primer coche fue un {coche1.Marca} {coche1.Modelo}");
Console.WriteLine($"Mi segundo coche fue un {coche2.Marca} {coche2.Modelo}");
1.1 - Readonly Fields with Init Accessors
When we use init accessors, they can only be used while initializing the object. This means they offer functionality similar to readonly
. In fact, if we try to assign a value to a field with init, it will tell us it is read-only.
For this reason, we can mutate readonly fields, as long as we do so from the init. Previously, we were only able to change the values of readonly fields from the constructor, but now we can do it from the init field.
We must change our class as follows:
public class Coche
{
private readonly string marca;
private readonly string modelo;
public string Marca
{
get => marca;
init => marca = (value ?? throw new ArgumentNullException(nameof(marca)));
}
public string Modelo
{
get => modelo;
init => modelo = (value ?? throw new ArgumentNullException(nameof(modelo)));
}
}
2 - Records in C#
In C#9, we’ll have the record
structure, which is a type (value type) that allows us to encapsulate the state of a property.
This means that the object or the information contained in a record will be immutable
.
To create a record, we just need to change our class as follows:
public record Coche
{
public string Marca { get; init; }
public string Modelo { get; init; }
}
As you can see, we’ve only changed the type, but what's the point? Using records lets us use new features; the main idea behind a record is for it to be immutable. Therefore, if we want to modify a record, what we need to do is make a copy of it. That’s where this feature makes sense.
2.1 - The with Expression in Records
With records, we can use a new way to create new values from existing ones by making a copy and then modifying what we need.
To make a copy, we need to use the with
keyword as follows:
Coche coche1 = new Coche { Marca = "Opel", Modelo = "Vectra" };
Coche cochedeSustitucion = coche1 with {Modelo = "Astra"};
Console.WriteLine($"Mi primer coche fue un {coche1.Marca} {coche1.Modelo}");
Console.WriteLine($"El coche de sustitución fue {cochedeSustitucion.Marca} {cochedeSustitucion.Modelo}");
As you can see, we use the object we want to copy followed by the with
keyword and then the modification we want to implement.
Records also support inheritance, while struct
for example, does not support it.
2.2 - Positional Records
When using records, we can implement positional constructors and deconstructors, meaning the variables are assigned in the order they are defined.
Going back to our Car record example:
public record Coche
{
public string Marca { get; init; }
public string Modelo { get; init; }
public Coche(string marca, string modelo)
=> (Marca, Modelo) = (marca, modelo);
}
We see that we have our constructor, but it’s a bit different from a regular constructor. Not only does it have a lambda expression, which is not usual, but the values are assigned in a peculiar way.
(Marca, Modelo) = (marca, modelo);
As mentioned, the values are assigned according to position. So Marca
will be assigned from marca
and Modelo
from modelo
. With the usual result in our console.Writeline
:
Mi primer coche fue un Opel Vectra
Of course, we must change the object instantiation as follows:
Coche coche1 = new Coche("Opel", "Vectra")
This means if we change the order of Marca
and Modelo
in the value assignments, they will be assigned differently:
(Modelo, Marca) = (marca, modelo);
//Result:
Mi primer coche fue un Vectra Opel
3 - Pattern Matching Improvements
As we know, in C# 8, the switch statement received improvements, but that was not enough. The C# language development team wanted to further improve the syntax to make it easier to understand when reading the code.
For this example, I added the CV
property to the record.
public record Coche
{
public string Marca { get; init; }
public string Modelo { get; init; }
public int CV { get; init; }
public Coche(string marca, string modelo, int cv)
=> (Marca, Modelo, CV) = (marca, modelo, cv);
}
3.1 - Relational Patterns
With relational patterns, we can use <
>
for less than and greater than:
static int CalcularSeguro(Coche coche) =>
coche.CV switch
{
> 100 => 1000,
< 20 => 100,
_ => 500,
};
3.2 - Logical Patterns
With logical patterns, we can use the keywords and
, or
, or not
, which correspond to `and`, `or`, and `not` negation.
static int CalcularSeguro(Coche coche) =>
coche.CV switch
{
> 100 => 1000,
< 20 and > 10 => 100,
_ => 500,
};
This way, we can increase the expressiveness of our expressions.
4 - Improvements in the new Expression
As we remember from C# 8, if we specify the variable type when instantiating an object, we don’t need to indicate the type because the compiler understands it from the variable type.
Coche coche1 = new ("Opel", "Vectra", 90);
Unfortunately, this feature wasn’t available for data collections, like lists in C# 9, but now it is.
var coches = new List<Coche>
{
new ("Opel", "Vectra", 90),
new ("Opel", "Astra", 85),
new ("Audi", "A3", 130),
};
5 - Top-level Programs
The C# development team seems to be taking the language a bit closer to other scripting languages, like Python.
What does this mean?
This is a typical structure for a console application in C# when you first open the editor, with a Hello World:
using System;
namespace CSharp9Ejemplo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hola Mundo");
}
}
}
As you can see, we don’t just have our hello world
statement, but also extra information, like the namespace
, the class
, the main method
, and even the arguments
passed as parameters.
This information is often unnecessary, especially when we’re learning. The C# team noticed this and wanted to address it by introducing top-level programs. These let you remove all the unnecessary boilerplate and keep just what you need: the using statement and the Console.Writeline()
statement.
using System;
Console.WriteLine("Hola Mundo");
The compiler understands what we want and takes care of everything else behind the scenes, keeping it transparent for the user.
This feature is very useful when writing small scripts. However, it’s not limited to small tasks, you can use any language library, for example Threading.Task
, to make concurrent HTTP requests.
If there is any problem you can add a comment bellow or contact me in the website's contact form