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
- Nullability in C# - What It Is and What It Is Not
- Null Conditional Operators in C# - ?. and ?[]
- Null Forgiving Operator in C# - !
- Null Coalescing Operators in C# - ?? and ??= (this article)
- C# "var" with a Reference Type is Always Nullable
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