The Best of C# 9.0

C# 8.0 introduced a lot of great new features and many of us wondered how C# 9.0 would follow such a tough act. Fortunately, C# 9.0 has continued to move the language forward by providing us with some great new tools.

Top Level Statements

Do you remember when all C# applications started like this?

using System;
namespace CSharpNine
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Well no more! Now we can get rid of all that Program/Main boilerplate to get just this:

using System;
Console.WriteLine("Hello World!");

I often spin up a new C# console app to test calling a web API through HttpClient. Now, that can be done in just these few lines of code:

using System;
using System.Net.Http;
var client = new HttpClient();
var response = await client.GetAsync("https://www.microsoft.com/");
Console.WriteLine($"status: {response.StatusCode}");

Init Only Setters

Object initializers are a very popular feature of C#. However, prior to C# 9.0, they had an important limitation in that they did not allow immutable properties. Consider the following Ninja class:

public class Ninja
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

We can create a ninja with an object initializer as follows:

var ninja = new Ninja { FirstName = "Naruto", LastName = "Uzumaki" };
ninja.FirstName ="Sasuke" //the change is allowed

Now, what if we wanted to prevent the FirstName of the Ninja from being changed after it has been initialized? If we were to remove the setter of the FirstName property, we wouldn’t be able use the object initializer to set it. C# 9.0 provides the solution with init only setters. We can modify our definition of the Ninja class so that the FirstName property has an init only setter:

public class Ninja
{
    public string FirstName { get; init; } //init only setter
    public string LastName { get; set; }
}

If we now tried to change the ninja’s FirstName after initializing it, the compiler would throw an error:

var ninja = new Ninja { FirstName = "Naruto", LastName = "Uzumaki" };
ninja.FirstName = "Sasuke"; // Compiler throws an error here

Pattern Matching

C# 9.0 allows pattern matching expressions for the is keyword with logical expressions such as and, not, and or. Here’s an example of a method using an or expression:

bool IsAppleOrOrange(string item) =>
    item is "Apple" or "Orange";

Using the not expression gives us a more readable, almost pythonic null check as follows:

if (item is not null)
    //do something

We can also use relational expressions such as <= and >= with the is keyword. For example, the following function returns whether the integer parameter is a valid percentage for a test score.

bool IsValidPercentage(int percentage) =>
    percentage is >= 0 and <= 100;

Logical and relational expressions can also be used with switch expressions which were introduced in C#8.0:

string GetScoreDescription(int percentage) =>
    percentage switch
    {
        <= 100 and >= 50 => "Pass",
        <= 50 and >= 0 => "Fail",
        _ => "Invalid Score"
    };

new()

C#9.0 allows you to create an instance of a class by using new() instead of the full constructor syntax when the type of the instance is known:

List<int> numbers = new();

This is a good alternative to the older convention of having the variable implicitly typed when instantiating it:

var numbers = new List<int>();

Records

C# 9.0 introduces record types which are similar to classes, but are meant to represent immutable data. Record types can be defined the same way as classes, but using the record keyword instead of class:

public record Ninja
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

Records use value-based equality. Basically, two records are equal if all their properties have the same value, even if the two records are different objects:

var naruto1 = new Ninja { FirstName = "Naruto", LastName = "Uzumaki" };
var naruto2 = new Ninja { FirstName = "Naruto", LastName = "Uzumaki" };
Console.WriteLine(naruto1==naruto2); //prints true

Since records are meant to be immutable, you can use the new with expression to create a modified copy of the record:

var naruto = new Ninja { FirstName = "Naruto", LastName = "Uzumaki" };
var boruto = naruto with { FirstName = "Boruto" };

Console.WriteLine(boruto.FirstName); //Prints Boruto
Console.WriteLine(boruto.LastName); //Prints Uzumaki

Console.WriteLine(naruto.FirstName); //Prints Naruto
Console.WriteLine(naruto.LastName); //Prints Uzumaki

Tags:,