In the last article, we took at look at
Nullability in C# - What It Is and What It Is Not. When we have nullability enabled, we need to respond to the warnings that
come up in our code. This often means performing null checks or giving the
compiler hints to what our code is doing. Fortunately, we have a number of
null operators in C# to help us with those tasks. To continue with the series
of articles exploring nullability, we will look at the null conditional
operators (represented by question mark dot '?.' and question mark square
brackets '?[]').
Articles
- Nullability in C# - What It Is and What It Is Not
- Null Conditional Operators in C# - ?. and ?[] (this article)
- Null Forgiving Operator in C# - !
- Null Coalescing Operators in C# - ?? and ??=
- 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
The null conditional operators give us a shortened syntax for checking for a
null object before calling a method, reading a property, indexing into an
object, or accessing another member on a nullable object.
Note: We will focus on the ?. operator for the samples since this is the
more common of the 2 operators.
Let's look at 2 blocks of code that are almost equivalent.
This first version checks to make sure that the "tokenSource" field is not
null before calling the "Cancel" method. If the field is null, then "Cancel"
is not called:
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
if (tokenSource is not null)
{
tokenSource.Cancel();
}
}
This second version also checks to make sure that the "tokenSource" field is
not null before calling the "Cancel" method. If the field is null, then
"Cancel" is not called:
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
tokenSource?.Cancel();
}
I say "almost equivalent" because the null conditional operator in the second
version gives us a little bit more -- it also helps with thread safety.
Let's look at these things in more detail.
"Possibly Null" Warning
We are looking at the same code as the
previous article. This can be found in the
"MainWindow.xaml.cs" file
in the "UsingTask.UI" project of the GitHub repository.
As a reminder, 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.
Our code has a nullable "tokenSource" field defined in the class (from the
"MainWindow.xaml.cs" file
noted above):
CancellationTokenSource? tokenSource;
public MainWindow()
{
InitializeComponent();
}
Since we have a "?" at the end of the field type "CancellationTokenSource", we
are indicating that this field can be null. (And since we are not assigning a
value to it in the constructor, it will be null initially).
This field is used in the Cancel button's event handler:
As we saw in the last article, in the code's initial state, we get a warning
when we access the "tokenSource" field:
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
tokenSource.Cancel();
}
The warning tells us that the field may be null and result in a null reference
exception.
CS8602: Dereference of a possibly null reference.
This gives us a warning that "tokenSource" may be null here. And if we call
the "Cancel" method on a null field, we will end up with a null reference
exception at runtime. (If you'd like to see this, it is shown in the
previous article.)
Checking for Nulls
The traditional way to check for nulls is to wrap the code in a guard clause.
This is often done with an "if" conditional:
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
if (tokenSource is not null)
{
tokenSource.Cancel();
}
}
The guard clause makes sure that the "tokenSource" field is not null before it
attempts to call the "Cancel" method.
***THREAD SAFETY WARNING***
When we have code that references class-level fields (or other variables
external to a method), there is a possibility of running into problems with
threading. This is especially important to keep in mind since so much of the
code we write today is asynchronous.
So what can happen here?
There is a brief period of time between when we check for the null in the "if"
condition and the when we actually run the "Cancel" method. In this brief
period it is possible that another method has set the "tokenSource" property
to null.
The result of this would be a null reference exception when the code tries to
call the "Cancel" method.
To get around this, developers will often make a local copy of the field or
variable and act on that, particularly when dealing with delegates. No one
else can affect the state of the local variable inside the method, so the
"Cancel" method could be called with confidence.
However, there is an easier way to deal with this thread safety issue: use the
null conditional operator.
Null Conditional Operator - ?.
The most common null conditional operator consists of a question mark and a
dot.
Note: The other null conditional operator uses a question mark with square
brackets - ?[]. This is used to access indexers, and we will look at this
briefly below. For more information, take a look at the Microsoft docs site
on null conditional operators: Member Access Operators.
Here is what this null conditional operator looks like in use:
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
tokenSource?.Cancel();
}
This has the same overall effect of the guard clause. At runtime, the
"tokenSource" field is checked for null. If the field is not null, then the
"Cancel" method is called normally.
If the field is null, the operation stops. The "Cancel" method is
not called.
But a shortened syntax is not all that we get with the null conditional
operator. We also get thread safety.
Thread Safety
The language designers took threading into account when they created this
operator. From the developer's perspective, the null check and "Cancel" method
call happen as a single operation, meaning that there is no possibility that
something else would be able to set the "tokenSource" field to null in the
middle.
I say "from the developer perspective" because the implementation is a little
more complex than that. If you're curious, you can always fire up ILDASM
(which is still included with the Visual Studio developer tools) and look at
the IL (intermediate language) that is generated by the compiler.
What About Return Values or Properties?
The next question is what happens if we use the null conditional operator to
call a method that returns a value or reads a property?
The short answer is that if the null conditional operator encounters a null,
then the return value or property will be returned as "null".
As an example, let's say that we only want to call the "Cancel" method if the
cancellation token is not already in a "canceled" state. Our "tokenSource"
field has a property called "IsCancellationRequested" that we can use to check
that.
Here's what that code may look like (note: this is not in the final code
sample):
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
if (!tokenSource?.IsCancellationRequested)
{
tokenSource?.Cancel();
}
}
This uses an "if" statement to check the "IsCancellationRequested" property on
the "tokenSource" field.
But we have some red squigglies. Here is the error message:
CS0266: Cannot implicitly convert type 'bool?' to 'bool'. An explicit
conversion exists (are you missing a cast?)
This tells us that "if" needs a non-nullable Boolean value ("bool"). But since
we use the null conditional operator, when we try to read
"IsCancellationRequested", we may get a "null" back (this is noted with the
nullable Boolean ("bool?") in the message).
We won't go through the trouble of fixing this code here. This sample is to
show what happens when we use the null conditional operator on something that
returns a value (either by calling a method or reading a property). In those
cases, we may get a "null" returned.
Null Coalescing Operator - ??
In a future article, we will take a look at the null coalescing operator that
lets us return a default value if we run across a null. This can help us fix
things like the scenario above and eliminate possible null values.
Null Conditional Operator - ?[]
The null conditional operator that consists of a question mark and a set of
square brackets is used for indexers on objects. The behavior is very similar
to using the ?. operator to access a property.
For example, let's consider the following code (note: this is not part of the
sample code on GitHub):
List<Person>? people = null;
Person? firstPerson = people?[0];
In this code, "people" is a nullable list of "Person" objects (and we set it
to null). When we try to index into the "people" list, we put a question mark
between "people" and the square brackets with the indexer.
If "people" is null, the indexer is not accessed, and so we do not get
a null reference exception here. The "firstPerson" variable would then be
assigned "null".
If "people" is not null, the indexer is accessed, and the first
item in the collection will be returned.
As a side note, if the list is empty, we will get an "Index Out of Range"
exception at runtime. But this is something we normally have to deal with
regardless of whether nullability is enabled.
So we can use the ?[] null conditional operator to index into a nullable
object. And this works similarly to using the ?. null conditional operator to
access a property on a nullable object. If the object is null, then we get a
null back. If the object is not null, then we get the item at the index or the
value of the property.
Wrap Up
The null conditional operators can help us in 2 ways. First they can shorten
our code by eliminating the need for a separate guard clause that specifically
checks for "null". Secondly, they give us thread safety during the null check,
so we do not need to worry about the possibility of a "null" sneaking into our
code in a multi-threaded or async scenario.
Enabling nullability gives us the warnings we need to eliminate possible nulls
in our code. And the null operators (including the null conditional operators)
can help us in dealing with those warnings.
In the next 2 articles, we will look at the null forgiving operator as well as
the null coalescing operators. These give us additional tools when dealing
with possible nulls in our code. Be sure to check back for more.
Next Article: Null Forgiving Operator in C# - !
Happy Coding!
No comments:
Post a Comment