Nullability Checks in .NET

Freedom from the dreaded NullReferenceException.

Posted by AgileCoder on June 17, 2022

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

Nullable reference types

?? (null-coalescing) and ??= (null-coalescing assignment) operators

! (null-forgiving) operator