Starting with .NET 6, new projects have nullable reference types enabled by
default. It is easy to get confused on exactly what that means, particularly
when migrating existing projects. Today, we'll take a look at what nullability
is and what it isn't. In future articles, we'll look at the null operators in
C# (null conditional, null coalescing, and null forgiving) -- these are all
various combinations of "?", "!", and ".".
Articles
- Nullability in C# - What It Is and What It Is Not (this article)
- Null Conditional Operators in C# - ?. and ?[]
- 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
Nullability Is:
- A way to get compile-time warnings about possible null references
- A way to make the intent of your code more clear to other developers
Nullability Is NOT:
- A way to prevent null reference exceptions at runtime
- A way to prevent someone from passing a null to your method or assigning a null to an object
Read on for details and examples.
Getting a Null Reference Exception at Runtime
Before looking at what nullability gives us, let's take a look at a project
that does not have nullability enabled. This can be found at the GitHub
repository noted above: https://github.com/jeremybytes/nullability-in-csharp. (See the README file of on the repository for information on how to run the
application yourself.)
The "StartingCode" folder contains a set of projects where nullability is
not enabled. Here is a screenshot of the running application (the
"UsingTask.UI" project in the solution):
And here is the code hooked up to to the "Cancel" button at the bottom
(specifically from the
MainWindow.xaml.cs file
in the "UsingTask.UI" project):
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
tokenSource.Cancel();
}
The problem with the code is that the "tokenSource" field in this code may be
null. (We won't go into the details of why that may be true; if you want more
information, you can look at the resources about Task and Cancellation
here: https://github.com/jeremybytes/using-task-dotnet6.)
If we run the application and then immediately click the "Cancel" button
(without clicking either of the other buttons first), we get a runtime error
-- a Null Reference Exception:
System.NullReferenceException: "Object reference not set to an instance of
an object."
This is because we are trying to call the "Cancel" method on a null
tokenSource.
Nullability and nullable reference types are there to help us prevent these
types of errors. So let's enable nullability and see what we help we get (and
what help we do not get).
Enabling Nullable Reference Types
As mentioned above, nullability is enabled by default when you create a
project with .NET 6. Nullability can also be enabled by editing the .csproj
file for projects that are upgraded from .NET 5 or .NET Core.
To enable nullable reference types, set the "Nullable" property to enable in
the .csproj file. Here is an excerpt from the
UsingTask.UI.csproj file
(note the code in the "StartingCode" folder has this commented out; the
"FinishedCode" has it uncommented):
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
Note: I also made this change to the
UsingTask.Library.csproj file
(but we will not look at this project until a later article).
Now that nullability is enabled, let's take a look at what this means for the
project.
With this property set, all reference types (whether they used as fields,
properties, method arguments, return types, or in other ways) are assumed to
be non-nullable. To make a reference type nullable, we need to explicitly
state that (and we will see how in just a bit).
Nullability Is: A way to get compile-time warnings about possible null references
The first thing we can do is see what messages we get. If we re-build the
solution, some green squigglies will show up to alert us to possible problems.
For example, there is now a warning on on the constructor for the MainWindow
class:
CancellationTokenSource tokenSource;
public MainWindow()
{
InitializeComponent();
}
If we hover over the warning, we can get the details:
CS8618: Non-nullable field 'tokenSource' must contain a non-null value when
exiting constructor. Consider declaring the field as nullable.
This message tells us that the "tokenSource" field will be null when the
constructor exits (and so the field will be null).
Another way is to open the "Error List" in Visual Studio to see each of the
warnings listed.
This includes the message above as well as 2 others (we will explore these in
later articles).
So Nullability is a way to get compile-time warnings about possible
null references.
Nullability Is NOT: A way to prevent null reference exceptions at runtime
Although we get the warnings (and that helps us), this does not prevent us
from building and running the application.
If we build and run the application again, and then click the "Cancel" button,
we get the same Null Reference Exception that we got before:
System.NullReferenceException: "Object reference not set to an instance of
an object."
Even though the "tokenSource" field is non-nullable, this is not enforced at
runtime. Since we do not assign a value to the "tokenSource" in the
constructor, it is null when we use it in the Cancel button's event handler.
So Nullability is not a way to prevent null reference exceptions at
runtime.
This is important to keep in mind when we are working with nullability. It is
helpful to us at compile-time, but it does not have a runtime effect.
Nullability Is NOT: A way to prevent someone from passing a null to your method or assigning a null to an object
As we saw above, these are compiler warnings (and those warnings are useful).
But they do not prevent someone from assigning "null" to an object or passing
"null" as an argument to a method.
As a very contrived example, we can assign a "null" to the "tokenSource" field
in the constructor:
CancellationTokenSource tokenSource;
public MainWindow()
{
InitializeComponent();
tokenSource = null;
}
We do get a warning:
CS8625: Cannot convert null literal to non-nullable reference type.
And the warning is useful. If we have our compiler set to fail on warnings
(which is used by many teams), then it would stop this code from compiling.
But there is nothing that forces another developer to have "fail on warnings"
turned on. The code is still buildable and runnable.
More subtly, if we are building a library that is used by another project,
that project could pass a null to one of our library methods (even if we have
it specified as non-nullable). So it is still very important that we check for
nulls in our code. We'll dive into this a little deeper in a subsequent
article.
So Nullability is not a way to prevent someone from passing a null
to your method or assigning a null to an object.
Nullability Is: A way to make the intent of your code more clear to other developers
One key feature of nullable reference types is that it can make the intent of
your code more clear. When we have nullability enabled in our code, all
reference types are non-nullable by default. If we want to have a field or
variable that is nullable, we need to mark it as such using "?".
The "tokenSource" field should be nullable. To let other developers (and the
compiler) know about this, we add a question mark at the end of the type when
we declare the field:
CancellationTokenSource? tokenSource;
public MainWindow()
{
InitializeComponent();
}
This means that the "tokenSource" field is allowed to be null, and the warning
that we got on the constructor is now gone.
But now that the "tokenSource" field is nullable, we get a different warning
in the Cancel button's event handler:
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
tokenSource.Cancel();
}
Here are the message details:
'tokenSource' may be null here.
CS8602: Dereference of a possibly null reference.
This tells us that we have a potential for a null reference exception at
runtime (and we have seen that happen several times already).
When we come across a message like this, we should have a guard clause that
does a null check. The way that we used to do this is to wrap the code in an
"if" statement:
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
if (tokenSource is not null)
{
tokenSource.Cancel();
}
}
This code makes sure that "tokenSource" is not null. If it is null, then it
skips over the code and does nothing. Otherwise, it will run the "Cancel"
method. This prevents the null reference exception at runtime.
Even if we click the "Cancel" button immediately after starting the
application, we do not get a null reference exception. This is because we have
a guard clause to prevent that.
The code above works, but there is an easier way of doing this with the null
conditional operator:
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
tokenSource?.Cancel();
}
We won't go into the details of the null conditional operator right now; that
is the subject of the next article.
Wrap Up
So we have seen that nullability and nullable reference types can be very
useful for sharing the intent of our code. It can also help us find potential
null reference exceptions in our application. But it does not stop us from
compiling, and it does not stop null reference exceptions from happening at
runtime.
Nullability Is:
- A way to get compile-time warnings about possible null references
- A way to make the intent of your code more clear to other developers
Nullability Is NOT:
- A way to prevent null reference exceptions at runtime
- A way to prevent someone from passing a null to your method or assigning a null to an object
The usefulness is very good, and it can save us a lot of time hunting down
bugs in our code. But we do need to be careful not to rely on it too heavily.
Next Article: Null Conditional Operators in C# - ?. and ?[]
Happy Coding!
Thanks for these great articles and thanks for not having Ads everywhere!
ReplyDelete