C# Nullable Reference Types

Kid on scateboard

“Brianna dropped the skateboard in front of Sam. “Don’t worry: I won’t let you fall off.” “Yeah? Then why did you bring the helmet?” Brianna tossed it to him. “In case you fall off.” ― Michael Grant, Hunger

Introduction

One of the major sources of unexpected runtime errors is null references. With nullable reference types you can make the compiler check for null references, and make it easier for you to avoid the dreaded NullReferenceException.

.NET 6 will enable nullable reference types by default, so if you did not know about this C# language feature, this is the time to learn about it!

Tony Hoare, the inventor of the null pointer, apologized even for inventing null pointers 😄

"Null References: The Billion Dollar Mistake"

Enabling null reference types

Nullable reference types are an opt-in feature, so you need to enable this for your project(s).

Open your project's Properties, and then on the build table you can enable Nullable from the drop-down list.

Or you can do this directly in the Project file:

<Nullable>enable</Nullable>

This will turn on nullable references for your whole project, you can also enable/disable this feature for sections of your code using #nullable pragma. Using nullable references does not have to be a big-bang change.

You can gradually update your code to support this. So if you have a region of code where you don't want the compiler to check for nullable references, you do this:

#nullable disable
string oldFashioned = null;
Console.WriteLine(oldFashioned);
#nullable enable

Using your first nullable reference

Let's try something. Start by declaring a variable of type String as follows:

string firstName = null; // warning
Console.WriteLine(firstName);

You will get a compiler warning, saying that you cannot assign null to a non-nullable type.

With nullable reference types enabled, the compiler will make each reference by default non-nullable, meaning that you cannot assign a null value to it.

Now let us add a nullable reference. The syntax is similar to the nullable value type syntax:

string? lastName = null;
Console.WriteLine(lastName);

Now you will not get a warning. That is because the ? after the type indicates that this reference can be null.

Nullable reference types use meta-data to allow the compiler do perform a deep check of your references, and to warn you when you are using an unchecked reference, which might result in a runtime error. There's no runtime difference between a non-nullable reference type and a nullable reference type.

Null forgiving operator

Try to assign a non-nullable reference type to a nullable reference and vice-versa:

lastName = firstName;
firstName = lastName; // warning

The first line will compile without any warnings, because lastName can take a string reference or null. The second line however will give you a warning, because firstName can only accept a reference to a string and not null. Because lastName can be null, the compiler emits a warning.

However, we can tell the compiler that we know what we are doing, using the null-forgiving operator !:

lastName = firstName;
firstName = lastName!; // no warning

Now the assignment does not give us a warning. This is a little like using a cast to tell the compiler that we are doing this on purpose.

Static Code Analysis

With nullable reference types, the compiler will analyse your code for possible NullReferenceExceptions and issue warnings. For example we have these two functions:

static string? CanReturnANull(bool returnNull)
  => returnNull ? null : "Hello";

static string CanNotReturnANull()
  => "World!";

When you call CanNotReturnANull the compiler does emit a warning because it knows this function cannot return a null value:

string result1 = CanNotReturnANull();
Console.WriteLine(result1.Length);

However, when calling CanReturnANull the compiler does imit a warning because this function can return a null value as indicated by the method's return type:

result1 = CanReturnANull(true); // warning
result1 = CanReturnANull(false); // warning

In this case you need to assign the CanReturnANull result to a nullable reference:

string? result2 = CanReturnANull(true);

Using this result in some expression will again result in a compiler warning because it can cause an exception:

Console.WriteLine(result2.Length);

You can remove the warning by adding a null check:

if (result2 != null)
{
  Console.WriteLine(result2.Length);
}

In this case you can hover the mouse over the result2 variable inside the if, and it will tell you that result2 is not null here. Hover over result2 outside the if and it will tell you that result2 can be null here. This the deep analysis at work!

Some applications for nullable reference types

The first reason to enable nullable reference types is less null checks.

For example a typical method normally needs to check its arguments for null values:

private static void OldFashioned(string s1)
{
  s1 = s1 ?? throw new ArgumentNullException(nameof(s1));
  // ...
}

With nullable reference types, there is no need since the compiler will issue a warning.

private static void UsingNullableRefTypes(string s1)
{
  // ...
}

Calling this method with possible null:

UsingNullableRefTypes(result2); // warning

And in places where you should have a null check it will again warn you.

Nullable reference types also make your code more expressive, it shows intent. Let us look at the CanNotReturnANull method again. This method returns a string type, without the ? type modifier.

static string CanNotReturnANull()
  => "World!";

This method says that is will never return a null. This method signature is honest, I can know that this method will not return a null because the type says so. So I don't need to check for null. If someone changes this method to return null, the compiler will issue a warning.

This is especially handy in interfaces, where we can again declare intent through the type.

Same thing for properties. Look at the Person class. The FirstName property can not be null, while the LastName property (yes there are countries that do not use a family name) can.

Cultures that don't use family names'

public class Person
{
  public string FirstName { get; set; }

  public string? LastName { get; set; }
}

Here the compiler will again emit a warning because there is only the default constructor which will leave the FirstName null. To get rid of it, we need to add a constructor that takes a non-null first name.

public class Person
{
  public Person(string firstName, string? lastName = default)
  {
    this.FirstName = firstName;
    this.LastName = lastName;
  }

  public string FirstName { get; set; }

  public string? LastName { get; set; }
}

Using Nullability Attributes

Sometimes you might want to help the compiler by explicitly defining the nullability of a variable.

AllowNull and DisallowNull

For example, you might want to have a property that returns a non-null default value when its value is null.

  public class Product
  {
    public int Id { get; set; }

    private string? displayName = null;

    public string DisplayName
    {
      get => displayName ?? this.Id.ToString();
      set => displayName = value;
    }
  }

This display property checks if the displayName field is null and returns a non-null value if so. So we declare this property to be non-nullable string.

However, we need the ability to reset the display name to null so it will use the fallback behavior.

If we do so, we get a compiler warning:

Product product = new Product();

product.DisplayName = "Choco";

product.DisplayName = null; // warning

We can tell the compiler that this is ok by adding the [AllowNull] attribute to the property.

[AllowNull]
public string DisplayName
{
  get => displayName ?? this.Id.ToString();
  set => displayName = value;
}

Now the compiler will no longer emit a warning:

product.DisplayName = null; // no warning

In a similar way we can use the [DisallowNull] attribute for a property that can return null, but may never be set to null.

NotNull

With the NotNull attribute you can tell the compiler that an argument will be non-null after the call.

Say that you need to method that will create an instance of a class like this:

private static void Initialize<T>(int id, [NotNull] ref T? result) where T : class
{
  Type t = typeof(T);
  result = Activator.CreateInstance(t, args: id) as T;
}

You can call this method with a null reference, the compiler will deduce that after this method (should it return instead of throwing an exception) the by-ref argument is non-null. That is why the Console.WriteLine will not issue a warning. You can verify this by hovering over the product2 variable.

Product? product2 = null;
Initialize<Product>(1, ref product2);
Console.WriteLine(product.DisplayName);

You could use this to ensure that a reference is not null, for example we will use an extension method that throws whenever a reference is null. By applying the NotNull attribute we tell the compiler that after this method returns we are sure that the reference is not null:

private static T IsNotNull<T>([NotNull] this T? x) where T : class
{
  if( x is null )
  {
    throw new ArgumentNullException();
  }
  return x;
}

We can now use this as follows without warnings:

Product? product3 = CreateProduct();
Console.WriteLine(product3.IsNotNull().DisplayName);

This is a very common scenario, so C# has the null-forgiving operator !. Although product3 can be null, we can tell the compiler that we are sure that it is not null, so we don't get warnings using it.

Product? product3 = CreateProduct();
Console.WriteLine(product3!.DisplayName);

What happens when product3 is null? Then we will get a NullReferenceException!

MaybeNullWhen

Sometimes you may want to return a reference from a method which can be null depending on circumstances. For example, add the TryParse method to the Product class:

public static bool TryParse(string info, out Product? p)
{
  if (info.StartsWith("P"))
  {
    if (int.TryParse(info.Substring(1), out int id))
    {
      p = new Product(id);
      return true;
    }
  }
  p = null;
  return false;
}

When we use this method to parse a string into a product we will still get a warning, even when the TryParse method is successful:

string productInfo = "P123";
if (Product.TryParse(productInfo, out Product? someProduct))
{
  Console.WriteLine(someProduct.Id); // someProduct may be null here
}

We can tell the compiler that the TryParse method will return a null reference when the result of the method is false but not when the result is true using the MaybeNullWhen attribute as follows:

public static bool TryParse(string info, [MaybeNullWhen(false)] out Product p)
{
  if (info.StartsWith("P"))
  {
    if (int.TryParse(info.Substring(1), out int id))
    {
      p = new Product(id);
      return true;
    }
  }
  p = null;
  return false;
}
string productInfo = "P123";
if (Product.TryParse(productInfo, out Product? someProduct))
{
  Console.WriteLine(someProduct.Id); // no more warning
}

Other attributes exist, check out MemberNotNullWhenAttribute or NotNullWhen...

Using Non-Nullable reference types with simple classes

Sometimes you need a class that has non-nullable properties but you can't have a single constructor to initialize all properties. Entity Framework comes to mind since materialization can use the default constructor to create the instance and then the property setters to initialize the instance's properties.

For example, the Category class will give you a warning for the CategoryName property:

namespace NullableReferenceTypes;

public class Category
{
  public int Id { get; set; }

  public string CategoryName { get; set; } // warning
}

You can fix this using the default! syntax. Here we assign a property to its default value, and using the null-forgiving operator we can remove the warning:

namespace NullableReferenceTypes;

public class Category
{
  public int Id { get; set; }

  public string CategoryName { get; set; } = default!;
}

Summary

With nullable reference types you can make the compiler do a lot of the work for you. Let the compiler find potential null-references, instead of finding them at runtime, especially in production!