C# 8: Nullable Reference Types

  • .NET

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. There are some documentation and blog posts that 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 is Nullable Reference Types and the different cases I've encountered while migrating a project to use this new feature of C# 8.

Why to 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# get 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. You can open the csproj file and add <LangVersion>8.0</LangVersion>:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>
</Project>

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

<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 the nullable reference types where you want. This can be very useful to migrate an existing code base. 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?).

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

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

// value can be null
public static 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:

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

Sometimes you know better than the compiler 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 value that is null. The compiler will consider the value as non-nullable and remove the warnings.

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:

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

Generic types

Generics type are specials as they can represent value types (struct) and references 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 possible to use some attributes to manage unconstrained generic types using attributes as explain later.

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 have multiple usages. For instance, a property can allow null in the setter but may return a non-null value in the getter.

private string _value = "";

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

void Demo()
{
    Value = null; // ok thanks to [DisallowNull]
    _ = Value.Length; // Currenty 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.

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 a null value but set it to a non-null value in the method.

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 the parameter type is string not string?
}

This opposite is also possible:

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

You can use the [NotNull] and [MaybeNull] attributes to express the nullability of the return value or the nullability of the out/ref parameters. While you can also use these attributes with any types, they seem to be useful only with unconstrained generic types as you cannot use T?.

// 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)
{
    ...
}

Conditional post-conditions attributes: NotNullWhen / MayBeNullWhen / NotNullIfNotNull

There are some methods where the return value maybe null or not depending on the return value. For instance Version.TryParse guaranties that the parsed version if 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.

// 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)
{
    ...
}

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

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 indicates 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 it affects the nullable analysis.

// 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");
}

This attributes can be used to ensure a value is not null.

string? value = null;    // value is null
MyAssert(value != null);
_ = value.Length;        // ok, value is not null here as MyAssert condition is false
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 to prevent a generic type to be nullable. For instance, TKey in Dictionary<TKey, TValue> cannot be null. If you try to add a value with key null, it throws an ArgumentNullException. To prevent using a nullable type as key, the generic parameter use the notnull constraint.

public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, IReadOnlyDictionary<TKey, TValue>, ISerializable, IDeserializationCallback
    where TKey : notnull
{
}

Handling constructors for deserialization or framework 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 indicates that the property is not initialized and may be null.

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:

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 case you don't want to add a constructor but still you want to remove those warnings. A solution is to initialize the properties using the default value and the null-forgiving operator.

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 fix in a future version of the compiler.

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;
}

Adding nullable annotations to an existing code base

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

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

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

    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?

Resources

Do you have a question or a suggestion about this post? Contact me on Twitter or by email!

Follow me:
Enjoy this blog?Buy Me A CoffeeDonate with PayPal