Wednesday, July 20, 2022

Null Coalescing Operators in C# - ?? and ??=

Over the last few articles, we have looked at nullability in C#. When we have nullability enabled, we need to respond to warnings that come up in our code. Sometimes when we come across a null value, we would like to replace it with a default non-null value. This is something that the null coalescing operator (2 question marks) can help us with.

Articles

The source code for this article series can be found on GitHub: https://github.com/jeremybytes/nullability-in-csharp.

The Short Version

When we have a null value, we often want to replace it with a non-null default value. The null coalescing operator can help us with this.

Here's an example of a traditional way to write this code:


    if (result is null)
        return new List<Person>();
    return result;

With the null coalescing operator, we can reduce this to a single line that include the null check and the non-null default value:


    return result ?? new List<Person>();

If "result" is not null, "result" is returned. If "result" is null, a new empty list is returned.

Let's look at this in more detail.

Possible Null Return

In the last article (about the null forgiving operator), we looked at a possible null return value for a method. We will start with that same method. The code is available in the GitHub repository for this article series: https://github.com/jeremybytes/nullability-in-csharp.

The "StartingCode" folder has the starting state of the code at the beginning of the first article in the series. The "FinishedCode" has the completed code. The links in this article will point to the "FinishedCode" folder unless otherwise noted.

As a reminder, the "GetAsync" method needs to return a non-null value, but in the "StartingCode", we get a possible null warning. (This is in the "PersonReader.cs" file of the "UsingTask.UI" project.)


    public async Task<List<Person>> GetAsync(
        CancellationToken cancelToken = new CancellationToken())
    {
        [collapsed code]

        var result = JsonSerializer.Deserialize<List<Person>>(stringResult, options);
        return result;
    }
    'result' may be null here.
    CS8603: Possible null reference return.

The "GetAsync" method returns a "Task<List<Person>>". The "List<Person>" part is non-nullable.

But because of the way the JsonSerializer works, we may end up with a "null" in the "result" variable. Because of this, we get a warning that we have a "possible null reference return".

Using a Guard Clause
As we saw in earlier articles, the traditional way of dealing with possible nulls is to set up a guard clause that lets us handle things appropriately. We can fix this code with an "if" statement:


    if (result is null)
        return new List<Person>();
    return result;

In this code, we first check to see if "result" is null. If it is, then we return a new empty list. If "result" is not null, then we return "result" directly. (Since the first "return" exits out of the method, we do not need an "else" block. We will only hit the second return if the "if" evaluates to false.)

Just like with the null conditional operator that we saw previously, we can get rid of the explicit guard clause by using the null coalescing operator.

Null Coalescing Operator - ??

The null coalescing operator is 2 question marks. Here is the same code block using this operator:


    return result ?? new List<Person>();

Here is how this operator works. Whatever is on the left side of the "??" operator (in this case the "result" variable) is checked for a "null". If the value is not null, then that value is returned. If the value is null, then whatever is on the right side of the "??" operator (in this case "new List<Person>()") is returned.

This works when we want to assign a value to a variable or field as well as when we want to return a value. Let's look at another example that assigns the result to a field.

Property with a Default Value

For this example, we will go back to the "MainWindow.xaml.cs" file in the "UsingTask.UI" project.

Here is the code that we will start with (this block is specifically from the file in the "StartingCode" folder):


    public MainWindow()
    {
        InitializeComponent();
        reader = new PersonReader();
    }

    private PersonReader? reader;
    public PersonReader Reader
    {
        get => reader;
        set => reader = value;
    }

This code has a non-null "Reader" property that uses a nullable "reader" field. (We'll go into the reasoning behind this in just a bit.) The constructor initializes the "reader" field to a non-null value.

Even though the "reader" property is not null when the class is created, we still get a warning in the "Reader" getter because the backing field is nullable (and it could be set to null elsewhere in the code).


    'reader' may be null here.
    CS8603: Possible null reference return.

Moving the Property Initialization

As a first step, let's move the field initialization out of the constructor and put it into the property getter.

For this, we will use a traditional guard clause (from the "MainWindow.xaml.cs" file of the "UsingTask.UI" project in the "FinishedCode" folder).

We will walk through several steps to get to a final state. This will help us better understand the code that we end up with. If you look at the file in the "FinishedCode" folder, you will find each of the steps in comment blocks.


    public MainWindow()
    {
        InitializeComponent();
    }

    private PersonReader? reader;
    public PersonReader Reader
    {
        get
        {
            if (reader is null)
                reader = new PersonReader();
            return reader;
        }
        set => reader = value;
    }

The getter for the property first checks the backing field to see if it is null. If it is null, then it assigns a value to it and then returns it. This ensures that the "Reader" property will never return a null.

Why Would We Do This?
You're probably wondering why we would change the code this way. The difference is pretty subtle. With either of the blocks of code, the "reader" field gets initialized. The difference is when the field is initialized.

In the first example, the field is initialized in the constructor. So it is set when the class is instantiated.

In the second example, the field is initialized the first time the "Reader" property is accessed. This means that it may never get set (if we never use the "Reader" property).

One place I have used this pattern is with property injection. Property injection is a good way to have a default value for a dependency that you can still swap out for unit testing. You can read more about a specific scenario where I've used this pattern here: Property Injection: Simple vs. Safe. (Hint: It involves serial ports that I do not want to initialize unless I actually use them.)

So assuming this is a pattern that we want to use, let's move on to integrating the null coalescing operator.

Adding the Null Coalescing Operator

Just like with our earlier example, we can remove the guard clause by using the null coalescing operator:


    private PersonReader? reader;
    public PersonReader Reader
    {
        get
        {
            reader = reader ?? new PersonReader();
            return reader;
        }
        set => reader = value;
    }

This code uses the "??" operator to check whether the "reader" field is null. If it is not null, then "reader" gets assigned to itself (so no change). If it is null, then a "new PersonReader()" is assigned to the "reader" field.

Either way, the "reader" that is returned in the property getter will not be null.

Combining Operators - ??=

The next step is to combine operators. The title and introduction make it look like "??" and "??=" are 2 separate operators. But technically they are not. Let's take a step back to look at something more familiar.

To increment a value, we can use the following code:
    int a = 3;
    a = a + 2;
This uses the "+" operator to add 2 to "a" and then assign the result back to "a". The effect is that the "a" variable is increased by 2.

But we can combine the "+" operator with the "=" operator to create the following:
    int a = 3;
    a += 2;
This has the same effect: the "a" variable is increased by 2.

We can do the same thing with the "??" operator. We can combine the null coalescing operator (??) with the assignment operator (=) to get ??=.

Here's what that looks like in code:


    private PersonReader? reader;
    public PersonReader Reader
    {
        get
        {
            reader ??= new PersonReader();
            return reader;
        }
        set => reader = value;
    }

This will do the null check on the "reader" field. If it is not null, then the value of "reader" is unchanged. But if "reader" is null, then a new PersonReader is assigned to it.

So just like with a +=, we can use ??= to combine the null coalescing operator with the assignment operator.

Making Things Shorter

We can further condense the code by combining the assignment of the "reader" field with the return of that field. This is where the code may get a bit confusing (which is why we're going in small steps):


    private PersonReader? reader;
    public PersonReader Reader
    {
        get { return reader ??= new PersonReader(); }
        set => reader = value;
    }

In our earlier example, we used the null coalescing operator to decide to return "result" (if it was not null) or a new empty list (if it was null).


    return result ?? new List<Person>();

We could do this with the property getter, but it would not set the "reader" field. So if the "reader" field was null, it would stay that way.

By using the combined operator "??=" the "reader" field is checked for null and assigned a value if it is null. Then the value of "reader" is returned.

It does take a little getting used to.

    get { return reader ??= new PersonReader(); }

We just have to remember that "??=" does both a null check and a possible assignment.

One Last Step: Expression-Bodied Getter
As long as we are making things short, we can make this a bit smaller by using an expression-bodied member for the getter.

Here's what that code looks like:


    private PersonReader? reader;
    public PersonReader Reader
    {
        get => reader ??= new PersonReader();
        set => reader = value;
    }

Since our getter only has a single expression (meaning one line of code that returns a value), we can use the "=>" operator and remove the curly braces and "return" keyword.

Expression-bodied members are a bit outside the scope of this article. But hopefully this syntax will make more sense if you run into it in someone else's code.

Wrap Up

The null coalescing operator (??) is very useful in eliminating nulls from our code. We can check for a null and provide a non-null default value. This way, we know that the result will not be null.

In addition, we can combine the null coalescing operator with the assignment operator - ??=. This gives us the ability to check a variable or field for "null" and then assign a valid default to it instead.

When we enable nullability in our projects, Visual Studio gives us compile-time warnings to help us eliminate unintended nulls. We can use the various null operators (null conditional, null forgiving, and null coalescing) to address those warnings.

The result is that we have code that is less likely to throw null reference exceptions at runtime. (And that means less debugging!)

Be sure to check the GitHub repository for the code samples and links to all of the articles: https://github.com/jeremybytes/nullability-in-csharp.

Happy Coding!

No comments:

Post a Comment