C# – Nullable Reference Types feature basics

The main purpose of the Nullable Reference Types (NRT) feature is to help prevent NullReferenceExceptions by showing you compiler warnings.

You can make a reference type nullable (ex: Movie? movie) or non-nullable (ex: Movie movie). This allows you to indicate how you plan on using these references. The compiler uses this info while analyzing actual usage and shows warnings if there’s a possibility of a NullReferenceException.

Here’s an example. Let’s say you have the following method:

string GetName()
{
    return null;
}
Code language: C# (cs)

This results in compiler warning CS8603 – Possible null reference return. Why? Because the method’s return type is string, which is a non-nullable reference type. This is like saying: “Don’t worry, I’m not going to return a null.” So when you do return a null, it’s setting the caller up for a NullReferenceException.

When you get a warning like this, you can decide how to fix it. Here’s three options for fixing this:

//1 - Don't return a null
string GetName()
{
    return "Bob";
}

//2 - Change it to a nullable reference type (string?)
string? GetName()
{
    return null;
}

//3 - Suppress the warning with the null-forgiving operator (!)
string GetName()
{
    return null!;
}
Code language: C# (cs)

Besides helping prevent NullReferenceExceptions, the NRT feature can also help you eliminate unnecessary null checks. Here’s an example:

void Process(Coder coder)
{
    coder.WriteCode();
}
Code language: C# (cs)

Since Coder coder is a non-nullable reference type, you can assume it won’t be null, and therefore skip the null check. Of course, this is the best case scenario and quite optimistic. This would require all code involved (including third party code) to be updated for this NRT feature and adhere to the convention of not using nulls with non-nullable reference types.

How to disable/enable Nullable Reference Types

Microsoft wisely made this as an optional feature. It was first introduced in C# 8 (.NET 5) and was opt in (disabled by default). In new projects created in .NET 6 and above, it’s opt out (enabled by default).

You can control this feature with the Nullable setting in the .csproj file. So for example, here’s how to disable it:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>disable</Nullable>
  </PropertyGroup>

</Project>

Code language: HTML, XML (xml)

Note: There are other options besides enable/disable, but these are the main ones that most devs will use.

You can also control this feature per source file by adding the #nullable directive, like this:

#nullable enable

public class Coder
{
    public string Name { get; set; }
    public string Projects { get; set; }
}

Code language: C# (cs)

Treating all Nullable Reference Type warnings as errors

You can treat all Nullable Reference Type warnings as errors, therefore preventing the code from compiling until someone deals with the problems. You can do that by adding <WarningsAsErrors>Nullable</WarningsAsErrors> to the .csproj file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <WarningsAsErrors>Nullable</WarningsAsErrors>
  </PropertyGroup>

</Project>

Code language: HTML, XML (xml)

Be aware that these errors can be ignored with the null-forgiving operator (!):

Coder coder = null!;
Code language: C# (cs)

So this is probably something to check during code reviews.

Not the same as nullable value types

It ? operator is used for both nullable value types and nullable reference types to change the nullability. This can be confusing, because they have the opposite meaning.

Value types (i.e. int) are non-nullable by default. They are initialized to the type’s default (ex: 0 for int). They aren’t null. Sometimes it’s really useful to know if a value type wasn’t set to anything (such as when you’re working with databases). This is why nullable value types were added. When you use a nullable value type like int?, it’s actually implemented by Nullable<int>. This means int? and int are not the same thing.

Compare this with reference types. These are null by default. A nullable reference type (string?) and non-nullable reference type (string) are compiled to the same thing. Adding the ? operator just tells the compiler that this could be null, so that it can warn against usage that could result in a NullReferenceException.