C# 9.0
It may be that .NET 5, the one and only .NET that will clear the confusion and lead the way for the next years was probably the biggest(?) announcement of Microsoft Build 2020, but there were numerous other equally important; from the general availability of the Blazor WebAssembly, the Azure Static Web Apps and all the projects related to IoT and Artificial Intelligence, all the way to .NET MAUI (short for Multi-platform App UI), Visual Studio Codespaces, Entity Framework Core 5, Project Tye, Azure Quantum and the multiple new features and capabilities of Azure Cosmos DB.
Although there were many more interesting things, C# 9 was left out intentionally because in this post we will deal with some of its exciting new features!
init
accessor
Up until C# 9, in order to use the object initializer syntax the properties of that object had to be mutable, which means they could change anywhere in the code even after object initialization. In other words, there was no way to use object initilizer on immutable properties, or even better a property had to be publicly accessible to use object initializer:
//In C# 8, a mutable object like the following allowed object initializer syntax
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
//A new instance of "Person" using object initializer syntax
new Person
{
FirstName = "George",
LastName = "Kosmidis"
}
//An attempt to use object initializer to create an instance in the following object would result in error
public class Person
{
public string FirstName { get; private set; }
public string LastName { get; }
}
// CS0272 The property or indexer 'Person.FirstName' cannot be used in this context because the set accessor is inaccessible
// CS0200 Property or indexer 'Person.LastName' cannot be assigned to -- it is read only
The init
accessor comes to solve this problem, by allowing the object initializer syntax but no field mutation after initialization:
//In C#9 you can use the "init" accessor that honours immutability
public class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
//A new instance can be created using object initializer syntax
var me = new Person
{
FirstName = "George",
LastName = "Kosmidis"
}
//But a change in a property value will throw an error
me.FirstName = "NewName";
// CS0200 Property or indexer 'Person.FirstName' cannot be assigned to -- it is read only
Immutability with readonly
The public string FirstName { get; private set; }
in the example is not really immutable because it is allowing changes within the object after object initialization. In C# 8, a truly immutable object that wouldn’t of course allow object initializer syntax would be like this:
public class Person
{
private readonly string firstName;
private readonly string lastName;
public string FirstName => firstName;
public string LastName => lastName
public Person(string firstName, string lastName)
{
this.firstName = (firstName ?? throw new ArgumentNullException(nameof(FirstName)));
this.lastName = (lastName?? throw new ArgumentNullException(nameof(LastName)));;
}
}
// -OR- since C# 6 using "readonly automatically implemented properties"
public class Person
{
public string FirstName { get; }
public string LastName { get; }
public Person(string firstName, string lastName)
{
this.FirstName = (firstName ?? throw new ArgumentNullException(nameof(FirstName)));
this.LastName = (lastName?? throw new ArgumentNullException(nameof(LastName)));;
}
}
The init
accessors can be used in similar scenarios where readonly
fields are necessary, because init
accessors can only be called during initialization and thus they are allowed to mutate readonly
fields:
public class Person
{
private readonly string firstName;
private readonly string lastName;
public string FirstName
{
get => firstName;
init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName)));
}
public string LastName
{
get => lastName;
init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName)));
}
}
Records
In a nutshell, records are a new lightweight immutable type that affects the immutability of an entire object -not just its properties-, thus making it behave more like a value (it should be seen more as data and less as object):
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
The data
keyword on the class declaration marks it as a record. Although they can have methods, properties or even operations they will still allow structural equality comparisons but not encapsulated state mutation. Instead, on each state change, new records should be created that will reflect this change, and C# 9 has a easy way to do this with the with
expression!
with
expressions
Immutable objects do not represent state over time (they cannot change!) but state at a specific point in time; a common practice to follow changes over time with immutable objects is to create a copy of the initial object changing only the properties that indeed changed, a process called non-destructive mutation
.
The with
expression comes to help this coding style, following object initializer syntax:
//An immutable object
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
//A new instance using object initializer since the "init" accessor was used
var me = new Person
{
FirstName = "George",
LastName = "Kosmidis"
}
//I didn't change to my brother over time, but it serves as a sample!
var myBrother = me with { FirstName = "Chris" };
Altering with
behavior with a custom constructor
Under the hoods a protected
constructor is implicitly defined and used by with
to copy property values from the original object, applying at the same time any changes defined. If the default with
behavior is not good enough, a “copy-constructor” can be explicitly defined and this one will be called instead:
//A record with an explicitly defined protected "copy-construtor" to be used with "with"
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
protected Person(Person original) {
this.FirstName = (original.FirstName ?? throw new ArgumentNullException(nameof(FirstName)));
this.LastName = (original.LastName ?? throw new ArgumentNullException(nameof(LastName)));
}
}
//Create a new instance of "Person"
var me = new Person
{
FirstName = "George",
LastName = "Kosmidis"
}
//Try to create my sister with null as FirstName, because I don't have a sister!
var mySister = me with { FirstName = null };
//throws System.ArgumentNullException: 'Value cannot be null. (Parameter 'FirstName')'
Syntactic sugar for Records
Since records are intended to be immutable, the defaults of a simple member declaration has changed:
//For records only, the following declarion doesn't mean the members are private
public data class Person {
string FirstName;
string LastName;
}
//On the contrary, it is equal to this:
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
Going a step further, records are implicitly defining positional constructor and destructor:
//The following
public data class Person(string FirstName, string LastName);
//is equivalent with this:
public data class Person
{
string FirstName;
string LastName;
public Person(string firstName, string lastName)
=> (FirstName, LastName) = (firstName, lastName);
public void Deconstruct(out string firstName, out string lastName)
=> (firstName, lastName) = (FirstName, LastName);
}
//or by also expanding the data members, to this:
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public Person(string firstName, string lastName)
=> (FirstName, LastName) = (firstName, lastName);
public void Deconstruct(out string firstName, out string lastName)
=> (firstName, lastName) = (FirstName, LastName);
}
Altering the default behavior of the positional constructor/destructor is possible by explicitly defining new ones.
Improved pattern matching
Pattern matching, initially added in C# 7 and improved in C# 8, is a way to test that a value has a certain shape, and extract information from the value when it has the matching shape. Pattern matching play a significant role in producing cleaner code for algorithms that are frequently needed today; for example extracting and consuming information from diverse resources that don’t share a common model and whose model isn’t even part of the original system.
Read a great tutorial about pattern matching in Microsoft Docs.
C# 9 added several new kinds of pattern, for simple, relational and logical patterns. Let’s expand our record above to include age and use it as a example:
public data class Person {
string FirstName;
string LastName;
int YearsWorking;
}
Simple type patterns
Currently, a type pattern needs to declare an identifier when the type matches even if that identifier is a discard using _
. Well, not any more:
//From this:
var experienceLevel =
person switch
{
Person p when p.Age <= 1=> ExperienceLevel.Low,
Person p when p.Age <= 5 => ExperienceLevel.Medium,
Person _ => ExperienceLevel.High
};
//To this:
var experienceLevel =
person switch
{
Person p when p.Age <= 1 => ExperienceLevel.Low,
Person p when p.Age <= 5 => ExperienceLevel.Medium,
Person => High //the underscore is gone
};
Relational patterns
Patterns that correspond to the relational operators (<
, >
, <=
,>=
) are introduced that will contribute to cleaner more readable code. Check how to example above is transformed:
//From this:
var experienceLevel =
person switch
{
Person p when p.Age <= 1 => ExperienceLevel.Low,
Person p when p.Age <= 5 => ExperienceLevel.Medium,
Person _ => ExperienceLevel.High
};
//To this:
var experienceLevel =
Person p when p.YearsWorking switch
{
<= 1 => ExperienceLevel.Low,
<= 5 => ExperienceLevel.Medium,
_ => ExperienceLevel.High,
};
Logical patterns
Finally, logical patterns (and
, or
, not
) are introduced that combine other patterns. They are spelled out as words to avoid confusion with operators that are used within a pattern:
//From this:
var experienceLevel =
person switch
{
Person p when p.Age <= 1 => ExperienceLevel.Low,
Person p when p.Age <= 5 => ExperienceLevel.Medium,
Person _ => ExperienceLevel.High
};
//To this:
var experienceLevel =
Person p when p.YearsWorking switch
{
<= 1 => ExperienceLevel.Low,
> 1 and <= 5 => ExperienceLevel.Medium,
_ => ExperienceLevel.High,
};
There are more…!
Here is a list of all the features coming to C# 9!
- Target-typed new
- Relax ordering of ref and partial modifiers
- Parameter null-checking
- Skip locals init
- Lambda discard parameters
- Native ints
- Attributes on local functions
- Function pointers
- Pattern matching improvements
- Static lambdas
- Records
- Target-typed conditional
- Extension GetEnumerator
- Module initializers
- Extending Partial
- Top-level statements
I didn’t write this list on my own, I took it from the Language Feature Status!