With the 5.0 SDK entering its latest and final preview, let us look at a couple of the most interesting new features that C# 9 will bring. Important to note is that all things can still change until the moment we see the final release.
When checking for nulls in our code we often like to write the shortest check possible. This often can be done via a ternary expression. But when dealing with Nullable types this often requires extra manual casting:
private void CurrentTernary()
{
int? x = null;
int? y = (x != null) ? x : (int?) -1;
//int? y = (x != null) ? x : -1; // wont compile
// Value of y is -1
// Of course we can currently use the shorthand null coalescing to improve readabililty
int? z = x ?? -1;
}
However, in C#9 we won’t have to include this extra cast, as long as both branches can be converted to the resulting type.
This casting is even more apparent with the null-coalescing operator. Where null can be evaluated even shorter.
private void CurrentNullCoalescing(Cube cube)
{
Pyramid pyramid = new Pyramid();
Shape shape = cube ?? (Shape) pyramid; // Cube and Pyramid share the same base class
}
private void FutureNullCoalescing(Cube cube)
{
Pyramid pyramid = new Pyramid();
Shape shape = cube ?? pyramid; // Cube and Pyramid share the same base class
}
C# 8 brought us expression matching to improve our switch cases. Although it was a huge leap forward it still had its limitations. With C# 9 we can evaluate expressions within the pattern matching code even more concise.
private void OldPatternMatching(int value)
{
bool isValidValue;
if (value > 0 && value < 100)
{
// Value must be between 0 and 100
isValidValue = true;
}
else if (value > 100)
{
// Value must be higher than 100
isValidValue = false;
}
else
{
// Value must be below 0
throw new ArgumentException();
}
}
private void CurrentPatternMatching(int value)
{
bool isValidValue = value switch
{
_ when value > 0 && value < 100 => true, // Value must be between 0 and 100
_ when value > 100 => false, // Value must be higher than 100
_ => throw new ArgumentException() // Value must be below 0
};
}
private void FuturePatternMatching(int value)
{
bool isValidValue = value switch
{
> 0 and < 100 => true, // Value must be between 0 and 100
> 100 => false, // Value must be higher than 100
_ => throw new ArgumentException() // Value must be below 0
};
}
A keen eye may also note that the keyword ‘and’ is used instead of ‘&&’. This keyword combined with ‘or’ and ‘not’ are also added which in my opinion improves readability of code dramatically:
private void CurrentNotExample(IShape shape)
{
if (!(shape is Pyramid))
{
// Do work
}
}
private void FutureNotExample(IShape shape)
{
if (shape is not Pyramid)
{
// Do work
}
}
I am really looking forward to these changes, the inclusion of the ‘not’ keyword will be invaluable when dealing with API responses. The concise way we can now write our switches will be food for thought when considering whether to use an if/else block or switch.
Where languages like Kotlin are built with immutability as a cornerstone, C# previously only had immutability if developers agreed to use it that way. For example, by having a public property with a private setter.
public class Vacation
{
public Vacation(
string destination,
DateTime startDate)
{
Destination = destination;
StartDate = startDate;
}
public string Destination { get; private set; }
public DateTime StartDate { get; private set; }
}
In the future we can create a ‘record’ marking the class as immutable. These classes should only be used for storing values and should not contain any other logic. With this record class we also add the ‘init’ initializer to our properties.
public data class Vacation
{
public string Destination { get; init; }
public DateTime StartDate { get; init; }
}
These properties can now only be set when creating the object and will result in a compilation error when attempted. (Note however if the modifier ‘data’ isn’t included, only the individual members are immutable and not the whole class).
public void CurrentVacation()
{
Vacation vacation = new Vacation("Essen", new DateTime(2020, 10, 21));
vacation.Destination = "Stay at Home"; // Error
}
public void FutureVacation()
{
Vacation vacation = new Vacation {
Destination = "Essen",
StartDate = new DateTime(2020, 10, 21) };
vacation.Destination = "Stay at Home"; // Error
}
This can even be combined with a readonly backing field, to ensure no code can modify it after initialisation:
public class Vacation
{
private readonly string destination;
private readonly DateTime startDate;
public string Destination
{
get => destination;
init => destination = value;
}
public DateTime StartDate
{
get => startDate;
init => startDate = value;
}
}
One of the pitfalls of immutability is that at some point eventually you will have to change some of the values of an object. Currently this can done via bulky methods like the AutoMapper package(which it is not intended for) or by creating a new object, copying all values and changing the one that needs to.
Record classes can be changed via ‘non-destructive mutation’, which take the approach of representing data over time instead of at a given time. The keyword ‘with’ in the code can specify what needs to be altered (this can be multiple values).
public void FutureVacationCopying()
{
Vacation vacation = new Vacation {
Destination = "Essen",
StartDate = new DateTime(2020, 10, 21) };
Vacation changedVacation = vacation with { StartDate = new DateTime(2021, 10, 20) };
}
Implicitly the Record classes implements a ‘copy constructor’ that will initialize all fields or change them if specified.
Furthermore, Records implicitly implement Value-based equality just like structs. Making equality checking much simpler. Of course, reference equals can still be used to distinguish between objects.
public void FutureEquality()
{
Vacation vacation1 = new Vacation {
Destination = "Essen",
StartDate = new DateTime(2020, 10, 21) };
Vacation vacation2 = new Vacation {
Destination = "Essen",
StartDate = new DateTime(2020, 10, 21) };
Vacation vacation3 = vacation1;
bool areEqual = vacation1 == vacation2; // Evaluates to true
bool areEqual2 = Equals(vacation1, vacation2); // Evaluates to true
bool areEqualReference = ReferenceEquals(vacation1, vacation2); // Evaluates to false
bool areEqualReference2 = ReferenceEquals(vacation1, vacation3); // Evaluates to true
}
Records also implicitly implement a deconstruct method that can be used to deconstruct a record into its values:
public void Deconstruction()
{
Vacation vacation = new Vacation {
Destination = "Essen",
StartDate = new DateTime(2020, 10, 21) };
(DateTime startDate, string destination) = vacation; // Via named deconstruction
var (destination, startDate) = vacation; // Via positional deconstruction
}
Combine data classes with improved pattern matching and you can do this (Where underscores can be used as wildcards):
public void PatternMatching()
{
Vacation vacation = new Vacation {
Destination = "Essen",
StartDate = new DateTime(2020, 10, 21) };
if (vacation is ("Essen", _))
{
// Positional pattern matching
}
}
Finally, if you have no need to override any default behavior a data class can be implemented in 1 line, which still can include inheritance.
// One line record declaration
public data class Vacation { string destination; DateTime startDate; }
// One line record declaration and initiation with inheritance
public abstract data class Trip { string destination }
public data class Holiday : Trip { DateTime startDate; }
private var holiday = new Holiday { StartDate = new DateTime(2020, 10, 21), Destination : "Essen", };
Adding immutability into C# is no small feat, and we as the community will have to see if current implementation will meet our needs. Either way I doubt that this is the last we see in this line of code changes.
As a last note I want to mention one thing that I am personally looking forward to and will probably not make the final release which is constant interpolated strings. Such a simple feature but will add more flexibility to constants by making them compile time. This will allow the developer to define ‘magic numbers’ only once while still using them in other constants.
public class ConstantInterpolatedStrings
{
private const decimal pi = 3.1415m;
private const string Sentence = $"Did you know you can also eat {pi}?";
}
The above example might not be the most serious take, but its inclusion will help with the DRY principle (Don’t Repeat Yourself).
It is always nice to see new features and with this feature set I think C# will catch up a bit to more recent languages like Kotlin. If you want to check out the new features coming to .Net 5.0 and C# you can download the SDK here and if you want to check the current status of the C# 9 development you can visit the official github page here.