Nullable Reference Types

Previous Versions

Previously in C#, references have been allowed to be null and they could be de-referenced without null checks. We would then get a NullReferenceException, this happens when we dereference a variable which is set to null. Sometimes we have nulls coming from other parts of the code, which we then discover later when the variable is de-referenced. We then have to spend time debugging to track down where the null came from in the first place.

New version

In C# 8, the treatment of nulls will become much more stricter than in previous versions of the language. This will take the form of compilation warnings on ordinary reference types like; object, string etc. Nullable Reference Types are not switched on by default, this needs to be switched on at project level or source file level. This can be done with the following line in a source file:

#nullable enable

Or the project file by adding the nullable tag to the property group, see the project file example below:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

The idea behind making this optional, is for large code bases, it will take time to modify the code to fix all the warnings that will be generated when this is switched on. Plus all the additional testing effort to ensure no new bugs have been unwittingly introduced. However, there is talk that in future releases that this will be switched on by default.

Non Nullable Reference Types

A non nullable reference type variable cannot be assigned null and doesn’t need to be checked for null before de-referencing.

In the console application example below, it is possible to get a null reference exception, if the console application is run without specifying an argument.

using System;

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string name = (args.Length > 0) ? args[0] : null;


            PrintName(name);


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }


        private static void PrintName(string name)
        {
            Console.WriteLine(name.Trim());
        }
    }    
}

If we turn on the Nullable Reference Types feature, like in the example below:

using System;
#nullable enable

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string name = (args.Length > 0) ? args[0] : null;


            PrintName(name);


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }


        private static void PrintName(string name)
        {
            Console.WriteLine(name.Trim());
        }
    }
}

We now get some compiler warnings for the console application, see screenshot below:

There is some magic performed by the compiler. So for non nullable reference types, the compiler uses flow analysis to ensure that local variables are assigned a non-null value when they are declared.

We can fix these warnings by removing the null assignment and replacing it with an empty string.

using System;
#nullable enable

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string name = (args.Length > 0) ? args[0] : "";


            PrintName(name);


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }


        private static void PrintName(string name)
        {
            Console.WriteLine(name.Trim());
        }
    }
}

Nullable Reference Types

Variables declared as a nullable reference type can be assigned null. They are not subjected to the same checks as their non nullable counterparts. The compiler will use flow analysis to ensure that any nullable reference type variable is checked for null before it is accessed or assigned to a non nullable reference type. The code example below demonstrates the warning. Note that the variable name is defined as a nullable reference type.

using System;
#nullable enable

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string? name = (args.Length > 0) ? args[0] : null;
            PrintName(name);

            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }

        private static void PrintName(string name)
        {
            Console.WriteLine(name.Trim());
        }
    }
}

The warning which is generated is in the screenshot below, it relates to the call to PrintName:

Adding the null check before calling PrintName fixes the warning. See the code example below.

using System;
#nullable enable

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string? name = (args.Length > 0) ? args[0] : null;


            if(name != null)
            {
                PrintName(name);
            }


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }

        private static void PrintName(string name)
        {
            Console.WriteLine(name.Trim());
        }
    }
}

The Null-Forgiving Operator

So what about the times where we know that the value is not null, but the compiler is still warning us? So in the code example below, we have that situation. Please note this is a contrived example.

using System;
#nullable enable


namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string? name = (args.Length > 0) ? args[0] : null;


            Console.WriteLine(name.Trim());


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }
    }
}

Let us assume that the user will always execute the console application with their name as an argument. We can placate the compiler warning by using the null-forgiving operator, the null forgiving operator is ! following the variable name as in name!.Trim(), in the example below:

using System;
#nullable enable

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string? name = (args.Length > 0) ? args[0] : null;


            Console.WriteLine(name!.Trim());


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }
    }
}

The compiler warning disappears when using the null forgiving operator.

Conclusion

The Nullable Reference Type feature will prevent a lot of defects in future code bases and improve existing code bases. It is currently switched off by default in C# 8. It will be switched on by default at some point in the future, so existing code bases will need to be updated and re-tested to allow for a smooth transition. Probably something that should be done sooner rather than later, especially if you are producing components which are consumed by others.

One other nice thing about using non nullable reference type variables is that we will not have to do any defensive programming when we de-reference them. This will reduce the amount of code that we have to write in the future.

The null-forgiving operator is a nice touch, to tell the compiler not worry. But there is always that chance that we have got it wrong, so having good unit tests and code reviews in place will make this less likely.

After code bases have been updated, it would probably be a good idea to switch on “treat all warnings as errors” on each project in a solution, to ensure warnings are dealt with and not ignored. If you go down that route, it would be sensible to have a unit test to inspect each project file to ensure that new projects are added with “treat all warnings as errors” and that it is not switched off accidentally.