Introduction to Nullable Reference Types
Prior to C# 8 we lived in a world where all reference types were nullable by default, and value types were non-nullable by default. If you wanted a nullable value type, you could designate it as such in the variable declaration by adding a ? to the type like this:
bool? triStateBoolean; // allows true, false, or null
Unfortunately the opposite case was not possible. There was no way to designate that a reference type could not be null. The resulting null reference exceptions are a common frustration for developers. In most cases, you get very little information about what object or property was null.
C# 8 introduced nullable reference types (a weird name, since they are nullable by default, but
that’s why I am writing this post). Then starting with .NET 6 and C# 10 all Microsoft provided project
templates enable the nullable context for the project by including the Nullable
element in
.csproj files like so:
<Nullable>enable</Nullable>
The result of having the nullable context enabled is that it causes the compiler to check and
warn when reference types declared without a ?
are assigned null, or violate other null check
conditions. Essentially, having the context enabled makes the complier treat reference types as
if they are non-nullable by default, just like value types. But, this results in just a warning.
If you want stricter checks, you can increase the compiler check level to treat these rule
violations as errors, rather than warnings by adding this to the .csproj file:
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
Reference Types in a Nullable Context
If the nullable context is enabled you will see the following behavior:
string normalString; //normal declaration
normalString = null; //results in a warning by default, or error if WarningsAsErrors configured
string? nullableString; // nullable declaration of a reference type
nullableString = null; // legal assignment to null
Gotchas and Details
Compile-Time vs. Runtime Behavior
The addition of nullable context and nullable reference declarations is compile-time
syntactic sugar. The runtime behavior of normalString
and nullableString
above will be
identical runtime and there are edge cases that can still be frustrating to track down. But the
compile-time checking goes a long way toward eliminating the dreaded NullReferenceException.
Two-part Annotation and Warning Context
Adding the <Nullable>enable</Nullable>
element to the .csproj file actually enables two
compiler features:
- It turns on the annotation context, meaning that the compiler will treat reference
types declared without the
?
suffix as non-nullable. - It turns on the warning context, meaning that if your code violates one of the nullable context check rules, a warning will be generated by the compiler.
You can control both of these features at a granular level by either changing the element in the .csproj file for a global change or adding adding compiler directives to your code where you want to change the behavior. Available options for the .csproj are:
<Nullable>enable</Nullable> <!-- enable annotations and warnings -->
<Nullable>annotations</Nullable> <!-- enable only annotations -->
<Nullable>warnings</Nullable> <!-- enable only warnings -->
If you want to control the behavior in a particular file you can use the directives:
#nullable [enable]/[disable] annotations //enables/disables the nullable ? suffix annotation
#nullable [enable]/[disable] warnings //enables/disables the nullable ? warning generation
If you use the directives, you can use the restore
option to reset behavior to the project level setting:
#nullable restore //revert both annotations and warnings to the project-level settings
#nullable restore annotations //revert annotation behavior only
#nullable restore warnings //revert warning generation behavior only
Nullable Context in Collection Types
If you are not careful, the positioning of the ?
suffix to declare nullability can trip
you up in collection or other complex types. Let’s look at an array of string
objects as
a simple example:
// NOTE: examples assume nullable enabled
// type?[] variableName... array can contain null values, but can't BE null
string?[] strings1 = new string?[] { "albert", "betty", null}; // no warning
string?[] strings2 = null; // compiler warning
// type[]? variableName... array can BE null, but cannot contain null values
string[]? strings3 = new string[]? { "albert", "betty", null}; // compiler warning
string[]? strings4 = null; // no warning
// type?[]? variableName... array can both CONTAIN null values, and BE null
string?[]? strings5 = new string?[]? { "albert", "betty", null}; // no warning
string?[]? strings6 = null; // no warning
Relevant Operators
C# has a number of operators available specifically for use with nulls and nullable types.
Null-Conditional Operators ?.
and ?[]
The null-conditional operators help prevent null reference exceptions by short-circuiting member or element access if the parent object or collection is null. For instance:
// at each step on the chain, if the object is null, then evaluation stops
// and selectedAddress is null
var selectedAddress = person?.Contacts?.Address;
// contrived example, but if a collection may be null you can use:
string[] names = null;
//using ?[] the assignment returns null without an IndexOutOfRangeException
string firstName = names?[0];
The Null-Coalescing Operator ??
The null-coalescing operator (??
) returns the value of the left side operand if it evaluates to a
non-null result. It is a short-circuiting operator, meaning that if the left side is non-null, then
the right side is not evaluated at all. If the left side evaluates to null, then the right side
operand is evaluated and returned, whether it evaluates to null or not.
This operator is very useful for argument checking:
public class SystemUser
{
private string? _department;
public string? Department
{
get => _department;
set => _department = value ?? throw
new ArgumentNullException(nameof(value), "Department cannot be null");
}
}
The Null-Coalescing Assignment Operator ??=
The null-coalescing assignment operator (??=
) assigns the value of the expression on the right side
to the operand on the left if the left hand operand evaluates to null. It is also a short-circuiting
operator.
The null-coalescing assignment operator allows you to handle conditional null checking and assignment in a single concise line:
// this verbose code...
if(someVariable is null) //note the pattern matching use of "is" rather than "=="
{
someVariable = someExpressionOrValue;
}
// can be replaced with
someVariable ??= expression
The Null-Forgiving Operator !
Sometimes the static check enabled by this nullability-check feature will return false warnings
where a value cannot be null. Rather that turn those warning off and on constantly in your code,
you can use the null-forgiving operator (!
).
For instance, let’s assume we have a IsPopulated()
function that enforces the business rule
that “All fields for a SystemUser must have a value”. If we write the following code, we
will get a warning when trying to access the Department property:
// local expression bodied function
bool IsPopulated(SystemUser? user) => user is not null && user.Department is not null;
var user = new SystemUser(...) //code to populate the user omitted.
if(IsPopulated(user))
{
Console.WriteLine(user.Department); // warning on access of Department
}
Since we are confident that neither user nor Department is null due to our IsPopulated guard function, we can instead override that warning using the null-forgiving operator:
bool IsPopulated(SystemUser? user) => user is not null && user.Department is not null;
var user = new SystemUser(...) //code to populate the user omitted.
if(IsPopulated(user))
{
Console.WriteLine(user!.Department); // possible null access is forgiven
}
I would personally urge caution when tempted to use the null-forgiving operator. Use the warning to be doubly sure that the code is doing what you expect, and a surprize null reference is not going to sneak in.
Microsoft Docs References For More Details
?? (null-coalescing) and ??= (null-coalescing assignment) operators