Exploring nullability improvements in C# and the !, ?, ??, and ??= operators.
Null is famously quoted as being the "billion-dollar mistake" due to the quantity of NullReferenceException
s that we see in code (particularly when just starting out).
The prevalence of null
forces a significant amount of developer attention doing things like:
- Validating that parameters are not
null
.
- Writing conditional logic to prevent
NullReferenceException
s from occurring.
- Defaulting variables to other values when
null
values are encountered.
Thankfully, a few years ago dotnet gave us some additional tools to detect and counteract null
values at compile time, to compete with similar features in languages like F# and others.
In this article I’ll walk through some of the nullability options we have when writing C# code as well as some of the supporting syntax that is handy when working with potentially null values.
Enabling Project-wide Nullable Checks
C# 8 introduced a radically different way of enforcing null reference exceptions at compile time using the project-wide nullable context.
This is an opt-in setting that will highlight potential null reference exceptions in your code during development. This helps you identify potential NullReferenceException
s before your program even runs.
My personal opinion is that I am almost always in favor of a stricter compiler if it can help me avoid bad code at runtime, but you may find this not as much to your liking.
In order to enable this setting, right click on your project in Visual Studio and then select “Properties”.
Once you are in properties, find the “General” blade under the “Build” menu and then change the “Nullable” setting to “Enable”. This will turn on project-wide nullable checks at compile time for that project.
Checking this modifies your .csproj
file and sets Nullable
to "enable
" within your PropertyGroup
element.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Once this setting is enabled, potential NullReferenceException
s are highlighted in the Visual Studio editor surface as shown below:
Here we see Visual Studio pointing out that _context
may currently be null given what it knows about your code. This indicates that a NullReferenceException
may occur when this line runs.
These warnings will also appear in your output from building the project and in your error list if you include Build + IntelliSense errors in that display as shown in the image above.
It’s worth noting that project-wide null checks are only available in C# 8 and later versions.
The Nullable Operator (?)
Once you turn on project-level null checks, all reference variables are assumed to be non-null unless you explicitly tell C# that null
values are possible in that variable.
We tell C# that null
are allowable in a variable using the nullable operator (?) as shown with the following declaration:
// _context may have a null value after initialization
private GameContext? _context;
// _graphics cannot have a null value after initialization
private GraphicsDeviceManager _graphics;
When you add the ?
operator after a Type declaration, you tell the C# compiler that a null
value is possible after the object has been instantiated. C# will then warn if you attempt to reference the value without checking for null.
If you omit the ?
operator, C# will assume that once the object is instantiated, null values are not possible in that member.
Thankfully, C# will also warn you if it is possible for something to be null
when it is declared as non-nullable.
See my articles on the required keyword and init-only members for more options for ensuring that values are provided at time of object initialization in more recent versions of C#.
The Non-Null Assertion (!)
While these null checks can be phenomenally helpful, they can sometimes miss the full context of your code and assume things are nullable that you know cannot be.
When this occurs, you can hint to C# that a variable is not null by adding the null-forgiving operator (!
) to the end of the variable reference as shown below:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
// _context will always be initialized before draw is called
_context!.Sprites.Begin();
_context.Update(gameTime, true);
_context.Sprites.End();
base.Draw(gameTime);
}
Here we add !
after the _context
variable is referenced the first time. C# now knows to suppress its nullability warning for this variable for this line. Since _context
has already been referenced before the next two lines run, they know a NullReferenceException
will never occur on the following two lines. The result of this is that !
effectively mutes false-positives for that variable for that method.
Note that if I am mistaken and I add !
to something that actually is null
at runtime, I will get a NullReferenceException
when that line runs.
To me, having to add !
in places is the most irritating aspect of the C# nullability improvements.
I’m finding that the irritation of using !
is subtly changing my coding style slightly to ensure that non-null things remain non-null at the end of initialization. Honestly, I think this shift in design philosophy is a key benefit of turning on nullability analysis.
By enforcing nullability warnings and irritating me with having to use the !
operator, C# is pushing me towards better runtime code quality.
Null-Conditional Operator (?)
Finally, let’s look at the ?
and ??
operators that help you safely deal with null
without having to use a lot of additional if
statements.
Without these operators, if you wanted to safely call something on a variable that might be null, you had to write an if
statement as shown below:
if (_context != null)
{
_context.Update();
}
However, with the ?
operator, you can condense this down into a single line of code:
_context?.Update();
This will invoke Update
on _context
if _context
is not currently null
. If _context
is null
, nothing will happen and the program will advance to the next statement.
See Microsoft’s documentation on the Null-conditional operator for more information.
Null Coalescing Operator (??)
The ??
operator is somewhat related. If you wanted to initialize a variable to one variable if it isn't null
or a fallback value if it is null
, you'd have to write some code like the following:
string displayText;
if (value != null)
{
displayText = value;
}
else
{
displayText = "Fallback value";
}
Now, you could write this with a ternary operator as string displayText = value != null ? value : "Fallback value"
, but ternary operators are fairly hard to read, even if you're familiar with them.
Instead, we have the null coalescing operator that lets us write the code as the following:
// use Fallback Value if value is null
string displayText = value ?? "Fallback value";
While this may be hard to read the first time you see it, I find this significantly more readable over time than the often-maligned ternary operator.
Null Coalescing Assignment Operator (??=)
Because the ??
operator caught on so nicely, and developers like concise code, we now have a related null coalescing assignment operator that helps us simplify code.
If you wanted to assign a value to a variable if the variable is currently null, you could write it as follows:
if (displayText == null)
{
displayText = "Fallback value";
}
Here we are simply assigning a value if something is currently null in a few lines of code.
The null coalescing assignment operator lets us do this in a single statement:
// Assign "Fallback value" to displayText only if displayText is currently null
displayText ??= "Fallback value";
I find the null-coalescing assignment operator a bit harder to read than the other operators I mention in this article, but even it isn’t bad and you’re not required to use it.
Final Thoughts
In this article I outlined how C# 8 and beyond allow you to use more advanced null
checking at compiler time and give you additional language features to help deal with the issues that can arise out of null
values and assignment statements.
As with any new language feature, these benefits of additional null safety come with the price of additional complexity in the form of new compiler warnings and new operators in our code. This added complexity can slow down and cause anxiety in new developers approaching the C# language.
However, the added benefits of early detection of null values and support for null-safe code patterns likely make up for the added complexity. Still, teams should be cautious about their use of any new language features and pick only the ones that give them the greatest value, knowing that they will need to teach new developers the meaning of some of these ever-increasing operators present in the C# language.