C# 8: Nullable Reference Types

 
 
  • Gérald Barré

This post is part of the series 'C# 8'. Be sure to check out the rest of the blog posts of the series!

I've used Nullable Reference Types for several weeks. Some documentation and blog posts were very useful to understand the concept and start using it. I've also looked at some projects such as CoreFX to find out how to handle some cases. This post explains what are Nullable Reference Types and the different cases I've encountered while migrating a project to use this new feature of C# 8.

#Why use nullable reference types?

C# 8 brings a new feature to solve the one billion-dollar mistake. The compiler will help you to find and fix most of your null-related bugs before they blow up at runtime. TypeScript has a similar functionality for a long time and it prevents so many potential issues. I'm very happy that C# gets the same feature. I hope you are prepared to fix tons of warnings 😃

#How to enable Nullable Reference Types?

First, you need to use C# 8 or later. You can open the .csproj file and add <LangVersion>8.0</LangVersion>:

MyProject.csproj (csproj (MSBuild project file))
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>
</Project>

Then, you can enable Nullable Reference Types at the project level or per file. To enable it at the project level you can add <Nullable>enable</Nullable> to the .csproj:

csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

To enable per file, you can use #nullable enable where you want to enable the functionality and #nullable disable where you want to disable it.

This means you can opt-in or opt-out of the nullable reference types where you want. This can be very useful to migrate an existing codebase. We'll see that later.

#Basic examples

By default, everything is non-nullable. If you want to declare a type as accepting null values, you need to add ? after the type. It is very similar to Nullable<T> (e.g. int?, bool?).

C#
void Sample()
{
    string str1 = null; // warning CS8625: Cannot convert null literal to non-nullable reference type.
    string? str2 = null; // ok
}

// value cannot be null
void ValueCannotBeNull(string value)
{
    _ = value.Length; // ok
}

// value can be null
void ValueMayBeNull(string? value)
{
    _ = value.Length; // warning CS8602: Dereference of a possibly null reference
}

If you call ValueCannotBeNull with a nullable type (e.g. string?), you'll get a warning:

C#
ValueCannotBeNull(null); // warning CS8625: Cannot convert null literal to non-nullable reference type

string? value1 = null;
ValueCannotBeNull(value1); // warning CS8625: Cannot convert null literal to non-nullable reference type

string? value2 = "test";
ValueCannotBeNull(value2); // ok, while the type of value2 is string?, the compiler understands the value cannot be null here

string value3 = "test";
ValueCannotBeNull(value3); // ok

The compiler understands conditional constructs and can infer that a nullable value is not nullable in some sections of the code. For instance, when you use if (value != null) the compiler understands the value is not null in the if block:

C#
void ValueMayBeNull(string? value)
{
    _ = value.Length; // warning CS8602: Dereference of a possibly null reference
    if (value != null)
    {
        _ = value.Length; // ok
    }
}

#The case of var type

In C#, you can use var to declare a variable without specifying its type. The compiler will infer it automatically based on the right expression. When nullable reference types are enabled, the compiler always considers the type is nullable. However, its flow analysis does understand that the value is not null when you assign the value with a non-null value.

C#
var demo = "string value";
// equivalent to
string? demo = "string value";

This choice was made to avoid lots of compilation warnings when enabling nullable reference types. Think about the following case:

C#
var demo = "string value";
_ = demo.Length; // ok because the compiler understands the value is not null here

demo = null; // Would fail if demo was string instead of string?
_ = demo.Length; // warning CS8602: Dereference of a possibly null reference

#Sometimes you know better than the compiler that something is not null

Sometimes the compiler is not smart enough to understand that something is not null in the context. In this case, you can use the null-forgiving operator (!) after the null value. The compiler will consider the value as non-nullable and remove the warnings.

C#
static void Main()
{
    var value = GetValue(true);
    Console.WriteLine(value!.Length); // in this case we know that value is not null, so we can use "!" to instruct the compiler "value" is not null here
}

static string? GetValue(bool returnNotNullValue)
{
    return returnNotNullValue ? "" : null;
}

You can also force null when something is not accepting null:

C#
string a = null;     // warning
string b = null!;    // ok
string c = default!; // ok

#Generic types

Generic types are special as they can represent value types (struct) and reference types (class). This means you cannot use the T? type to represent a nullable type as it would conflict with the existing Nullable<T> when the generic is a value type. To use this syntax you need to constrain the generic to a reference type or a value type. It is possible to use some attributes to manage unconstrained generic types using attributes as explained later.

C#
public static void ReferenceType<T>(T? value) // [Nullable(2)]T value because T is a class
    where T : class
{
}

public static void Test2<T>(T? value) // Nullable<T> because T is a struct
    where T : struct
{
}

#Preconditions attributes: AllowNull / DisallowNull

Some cases cannot be expressed only with a nullable type. This may be the case when the type has multiple usages. For instance, a property can allow null in the setter but may return a non-null value in the getter.

C#
private string _value = "";

[AllowNull]
public string Value
{
    get => _value;
    set => _value = value ?? "";
}

void Demo()
{
    Value = null; // ok thanks to [DisallowNull]
    _ = Value.Length; // Currently there is a warning but this should be fixed in the future version of the compiler
}

You can also do the opposite. A property can disallow null in the setter but may return a nullable value in the getter.

C#
private string? _value = null;

[DisallowNull]
public string? Value
{
    get => _value;
    set => _value = value ?? throw new ArgumentNullException(nameof(value));
}

void Demo()
{
    Value = null; // not ok because of [DisallowNull]
    _ = Value.Length; // not ok as Value may be null
}

Another case is the ref parameters. You may want to accept null values but set it to a non-null value in the method.

C#
public void DemoRef([AllowNull]ref string value)
{
    value = "";
}

void A()
{
    string? value = null;
    DemoRef(ref value); // ok, DemoRef allows nullable input
    _ = value.Length;   // ok, value is non-null here the parameter type is string not string?
}

This opposite is also possible:

C#
public void DemoRef([DisallowNull]ref string? value)
{
    value = null;
}

void A()
{
    string? value = null;
    DemoRef(ref value); // warning value is null but the method doesn't allow null as input
    _ = value.Length;   // warning value is null as the method may change the value of the ref parameter to null
}

#Post-condition attributes: NotNull / MaybeNull / MemberNotNull

You can use the [NotNull] and [MaybeNull] attributes to express the nullability of the return value or the nullability of the parameters when the call returns. While you can also use these attributes with any type, they seem to be useful only with unconstrained generic types as you cannot use T?. C# 9 update: The ? operator now works on unconstrained generic types. So, you should use T? instead of the [MayBeNull] attribute.

[MemberNotNull] indicates that the method or property will ensure that the listed field and property members have values that aren't null.

C#
// The return value may be null if T is a reference type and no item matches the condition.
[return: MaybeNull]
public static T FirstOrDefault<T>(IEnumerable<T> items, Func<T, bool> match)
{
    ...
}

// value may be null on input, but is non-null when the method returns
public static void SetValue<T>([NotNull] ref T value)
{
    ...
}

// C# 9: T? is allowed on unconstrained generic type
public static T? FirstOrDefault<T>(IEnumerable<T> items, Func<T, bool> match)
{
    ...
}
C#
public class C
{
    // Not nullable as it is initialized in the constructor using the Init method.
    private string _s;

    public C() => Init();

    [MemberNotNull(nameof(_s))]
    private void Init() => _s = "Hello";
}

You can create a method to assert the value of an object is not null as explained in this comment:

C#
public static void AssertNotNull<T>([NotNull]T? obj)
    where T : class
{
    if (obj == null)
        throw new Exception("Value is null");
}

void Sample()
{
    string? test = null;
    AssertNotNull(test);
    _ = test.ToString(); // test is not null here
}

#Conditional post-conditions attributes: NotNullWhen / MayBeNullWhen / NotNullIfNotNull / MemberNotNullWhen

There are some methods where the return value may be null or not depending on the return value. For instance Version.TryParse guarantees that the parsed version is not null if the method returns true. You can also want to indicate that a value may be null when the return value is false. This is the case of Dictionary.TryGetValue. To express that you can use [NotNullWhen] and [MaybeNullWhen]. The last conditional post-conditions is [NotNullIfNotNull]. It indicates that the return value is not null when a specific parameter is not null.

C#
// result may be null if TryGetValue returns false
public bool TryGetValue(TKey key, [MaybeNullWhen(returnValue: false)]out TValue result)
{
    ...
}

// result is not null if TryParse returns true
public static bool TryParse(string? input, [NotNullWhen(returnValue: true)] out Version? result)
{
    ...
}

// The return value is not null if the value of the path parameter is not null
// note that the name of the parameter is not validated, be careful about typo
[return: NotNullIfNotNull(parameterName: "path")]
public static string? ChangeExtension(string? path, string? extension)
{
    ...
}

public sealed class C
{
    // If IsNotNull returns true, Value is not null
    [MemberNotNullWhen(returnValue: true, member: nameof(Value))]
    public bool IsNotNull => Value != null;

    public string? Value { get; set; }
}

The support of the NotNullWhen and MaybeNullWhen attributes is very basic at the moment. It only handles cases where the method is called in an if statement:

C#
if (Version.TryParse("", out var version))
{
    _ = version.Major; // ok, version is not null as TryParse returns true
}

var parsed = Version.TryParse("", out var version);
if (parsed)
{
    _ = version.Major; // warning, the compiler is not able to understand that the method returns true here
}

#Flow attributes: DoesNotReturn / DoesNotReturnIf

Flow attributes allow to indicate the compiler that no nullable analysis needs to happen after that point, since that code would be unreachable. Here's how to use these attributes and how they affect the nullable analysis.

C#
// This methods never returns
[DoesNotReturn]
public static void ThrowArgumentNullException(string argumentName)
{
    throw new ArgumentNullException(argumentName);
}

// This method does not return if condition is false
public static void MyAssert([DoesNotReturnIf(false)] bool condition)
{
    if (!condition)
        throw new Exception("condition is false");
}

These attributes can be used to ensure a variable/parameter is not null.

C#
string? value = null;    // value is null
MyAssert(value != null);
_ = value.Length;        // ok, value is not null here as MyAssert condition is false
C#
void Sample(string? value)
{
    if (value == null)
        ThrowArgumentNullException(nameof(value));

    _ = value.Length; // ok, value is not null here
}

#Generic constraint notnull

The notnull constraint allows preventing a generic type from being nullable. For instance, TKey in Dictionary<TKey, TValue> cannot be null. If you try to add an item with a null key, it throws an ArgumentNullException. To prevent using a nullable type as the key, the generic parameter uses the notnull constraint.

C#
public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, IReadOnlyDictionary<TKey, TValue>, ISerializable, IDeserializationCallback
    where TKey : notnull // TKey cannot be null (e.g. string? is invalid)
{
}

#Handling constructors for deserialization or frameworks such as AutoMapper

If you declare a property as non-nullable, it must be initialized in the constructor. Otherwise, the compiler raises a warning to indicate that the property is not initialized and may be null.

C#
public class Post
{
    public string Title { get; set; } // warning CS8618 Non-nullable property 'Title' is uninitialized
    public string Uri { get; set; }   // warning CS8618 Non-nullable property 'Uri' is uninitialized
}

You can add a constructor and initialize the properties to remove the warnings:

C#
public class Post
{
    public Post(string title, string uri)
    {
        Title = title;
        Uri = uri;
    }

    public string Title { get; set; }
    public string Uri { get; set; }
}

There are some cases where the class is never instantiated directly, but using deserialization (JSON, XML, etc.) or using frameworks such as AutoMapper or IOptions in ASP.NET Core. In these cases, you don't want to add a constructor but you want to get rid of those warnings. A solution is to initialize the properties using the default value and the null-forgiving operator.

C#
public class Post
{
    public string Title { get; set; } = default!; // ok, as the property is initialized
    public string Uri { get; set; } = default!;
}

#TryParse/TryGetValue pattern and generics

Using TryParseXXX or TryGetXXX methods is a very common pattern. As seen before, you can use the [NotNullWhen(true)] attribute to indicate that the value of the out parameter is not null when the result of the method is true. However, in the case where the method returns false, you want to assign the out parameter with the default value. Currently, the compiler emits a warning when you do that. The workaround is to use the null-forgiving operator. Note that this is a bug that will be fixed in a future version of the compiler.

C#
public static bool TryParse<T>(string? input, [MaybeNullWhen(returnValue: false)] out T result)
{
    if (input != null)
    {
        result = Activator.CreateInstance<T>();
        return true;
    }

    // The null-forgiving operator is mandatory here. This may be fixed in a future version of the compiler.
    // https://github.com/dotnet/roslyn/issues/30953
    result = default!;
    return false;
}

#Useful extensions methods

The compiler has limitations on what it can infer and express concerning nullability, especially in the context of generics. When using LINQ, it's common to deal with sequences that should not contain null values. Unfortunately, there is no easy way to make the type checker aware of when they got removed.

C#
List<string?> values = new List<string?> { "a", null, "b" };
list.Where(x => x is not null)
    .Select(x => x.Length); // Warning CS8618: Non-nullable value type 'string' cannot be null

You can create extension methods to handle this case:

C#
static class EnumerableExtensions
{
    public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
        where T : class
    {
        return source.Where(item => item != null)!;
    }

    public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> items)
        where T : struct
    {
        foreach (var item in items)
            if (item.HasValue)
                yield return item.GetValueOrDefault();
    }
}

Here're some examples of how to use the extension methods:

C#
List<string?> values = new List<string?> { "a", null, "b" };
list.WhereNotNull()
    .Select(x => x.Length); // ok
C#
List<int?> values = new List<int?> { 1, null, 2 };
list.WhereNotNull()
    .Select(x => x.ToString("X")); // ok as x is a int instead of int?

#Adding nullable annotations to an existing code base

I think that the opt-out strategy is easier to manage as it quickly allows us to see the number of types that need to be annotated.

  1. Change the project configuration to use C# 8 and enable the nullable analysis

  2. Add #nullable disable at the top of each .cs file. You can use the following PowerShell script to do it automatically:

    PowerShell
    Get-ChildItem -Recurse -Filter *.cs | ForEach-Object {
        "#nullable disable`n" + (Get-Content $_ -Raw) | Set-Content $_
    }
  3. For each file remove the directive, add the nullable annotations, and fix warnings. Start with the files that have the least dependencies. This way the new warnings should only appear in the file where you remove the directive.

You have finished when there are no more #nullable disable in your code!

#Targeting .NET Standard 2.0 and .NET Core < 3.0

Check the next post: How to use Nullable Reference Types in .NET Standard 2.0 and .NET Framework?

#Checking nullable reference types at runtime

The nullable reference types are only checked at compile time. If you want to check them at runtime, you can use the NullableContext property of the System.Runtime.CompilerServices.RuntimeHelpers class. This property returns the nullable context for the current method. The value is 0 if the nullable context is disabled, 1 if it is enabled, and 2 if it is enabled and warnings are enabled.

C#
var nullableContext = new System.Reflection.NullabilityInfoContext();
var propertyInfo = typeof(string).GetProperty("Length");
var info = nullableContext.Create(prop);
var nonNullable = info.ReadState == NullabilityState.NotNull;

#Resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub