Blog

C# 8: nullable reference types

March 20, 2019 | 5 Minute Read

Nullable reference types will change the code we write today.


C# 8.0 Series

Want to read my other posts about C# 8?

Prerequisites & Setup

To play with this feature you need Visual Studio 2019 and .NET Core 3.0 SDK. For existing code, this feature must be opt-in for making reference types non-nullable. And this for a good reason: it would break a lot of code because by default every reference type is assumed to be non-nullable (never null). As soon C# 8.0 is stable, the feature will be enabled by default for new projects.

We need to modify the .csproj file to enable it:

<LangVersion>8.0</LangVersion>
<NullableContextOptions>enable</NullableContextOptions>

Let’s try it out

C# has two kind of types: value types (e.g. int, byte, structs) and reference types (e.g. class, interface). Reference types can be null by design and this introduces additional complexity while writing code. Especially when the project is fairly large and complex.

Imagine, we have a CarWashSalon class with a method Wash that doesn’t accept null as input parameter:

public class CarWashSalon
{
	public void Wash(Car car)
	{
		if (car == null)
		{
			throw new ArgumentNullException(nameof(car));
		}

		// wash the car
	}
}

Looks quite common, right? There isn’t a way to express (at compile time) the intension of our design - car isn’t expected to be null. Calling the method with a null reference will end up with an ArgumentNullException at runtime (or even worse with a NullReferenceException…):

class Program
{
	static void Main(string[] args)
	{
		var carWashSalon = new CarWashSalon();

		Car myCar = null;

		carWashSalon.Wash(myCar);
	}
}

Wouldn’t it be awesome to be able express our design decision and get warnings (or errors) when it is not respected? This is exactly the purpose of the nullable reference types feature. The compiler (roslyn) analyzes the code and instantly tells us about the null assignment issue:

non-nullable errors

  • We can’t assign null to the myCar variable because it’s non-nullable.
  • We can’t pass null (in this case myCar) to void CarWashSalon.Wash(Car car) because it expects a non-nullable.

Everything is fine when we assign an instance of a Car to myCar:

Car myCar = new Car();

But wait! What if myCar could be null (be a nullable reference type) by design? The new nullable reference type operator is solved elegantly - it’s the same as for nullable values types. By suffixing Car with a ? it becomes a nullable reference type:

class Program
{
	static void Main(string[] args)
	{
		var carWashSalon = new CarWashSalon();

		Car? myCar = null;
		
		carWashSalon.Wash(myCar);
	}
}

Guess what happen? An error is back on our error list - CarWashSalon.Wash() still doesn’t accept null.

The compiler does control-flow analysis and detect when we try to dereferencing null. To solve the issue we could use a if statement to ensure null is not passed into the Wash method:

class Program
{
	static void Main(string[] args)
	{
		var carWashSalon = new CarWashSalon();

		Car? myCar = null;

		if (myCar != null)
		{
			carWashSalon.Wash(myCar);
		}
	}
}

Otherwise, when we guarantee that myCar is never null the error is gone as well - even myCar is still nullable:

	Car? myCar = new Car();
	carWashSalon.Wash(myCar);

Under some conditions we would like to tell the compiler, that we know what we are doing and we are aware of the null. This situation can be expressed with the null-forgiving operator !:

	Car? myCar = null;
	carWashSalon.Wash(myCar!);

As an alternative we could change the nullable context to be disabled for a specific code block:

Car? myCar = null;
#nullable disable
    carWashSalon.Wash(myCar);
#nullable restore

Technical details

When you are curious as myself, then you wonder how is a nullable reference type represented when the code is compiled into an assembly. There is no added type to the type system, but somehow the tooling is able to distinguish between nullable- and non-nullable reference types. A closer look with IL-Spy reveals the secret:

nullable types CIL

There is a NullableAttribute applied to the members with value 1 (non-nullable) and 2 (nullable). To avoid external dependencies, the attribute is embedded into the assembly dynamically:

nullable types CIL

Conclusion

In my opinion this is the most impactful feature of C# 8.0. It improves code quality and could avoid the famous NullReferenceException in most scenarios.