C# Nullable References

The Biggest New Thing in C# for Years

C# logo overlaying code

But hang on - haven't reference types always been nullable?

That's absolutely right. It's fundamental to the way a reference type works. The variable doesn't contain the data itself, but a reference to a separate memory location where the data is stored. Thus, if there's no data, the reference is set to null.

What's actually new in C# version 8 is non-nullable references. And that's potentially a breaking change, of which more anon.

First Came Nullable Values

Originally in C#, values could never be null, because a variable of a value type actually contains the data, not a reference to it. This doesn't always make sense. For example:

DateTime dateOfDecommission;

What if our object - whatever it may be - hasn't yet been given a decommission date? We could give it special value such as DateTime.MinValue or DateTime.MaxValue, but those are actual values - far into the past or far into the future. They are valid dates and we'd always need to have specific coding to check for them.

And so C# version 2 introduced nullable value types:

DateTime? dateOfDecommission = null;

In addition to all the other values the variable may have, it can take on the value null. This doesn't turn it into a reference type - the variable still contains the data - but the data structure has an extra boolean field added to it, which we can access:

if (dateOfDecommission.HasValue) ...

More usually we just compare to null, which the compiler interprets as checking the HasValue flag.

if (dateOfDecommission == null) ...

So Why Nullable References?

Consider the following UML:

Here we have two different relationship types - association and containment - which we could read in English as a MotorCar may have a Driver, but must have an Engine. (Whether this is correct will depend on the context. For software dealing with an assembly line, a car would never have a driver and might or might not have an engine. But for a car hire company, the diagram would make sense.)

Until now, there's been no way of expressing this in C#. We just had references:

public class MotorCar
private Driver _driver;
private Engine _engine;

It's up to the developer - to all the developers in the team - to remember that _driver can be null and _engine cannot.

Can You Guess the Syntax?

In C#8, declaring a nullable reference is just the same as declaring a nullable value - we use a question mark:

public class MotorCar
private Driver? _driver;
private Engine _engine;

And that's what makes it a breaking change. In C#8, _driver is actually what it always was - a nullable reference. It's _engine that has changed. In C#7 it was nullable, now it's non-nullable.

It's All Optional

For that reason, this feature is opt-in. We can do it on a per-file basis with

#nullable enable

or we can set it for an entire project in the .csproj file. Otherwise, all references are treated just as in C#7.

And even if we turn the feature on, any breaches of the rules will come out as warnings, not errors.

So What are the Rules?

Nullable and non-nullable references are identical in the compiled code. The only difference is in the rules that are applied during compilation. To begin with, the above code fragment will produce a warning. By default, reference class members are initialized to null, but _engine cannot be null. We need to fix this somehow, either with a constructor or in line:

public class MotorCar
private Driver? _driver;
private Engine _engine = new Engine();

But the big benefit comes with code like this:

public override string ToString()
return $"Car with capacity {_engine.Capacity}cc is driven by {_driver.Name}.";

There's a problem here: we haven't checked whether those references are null. If either _engine or _driver is null, then we'll get a NullReferenceException, and ideally we should be putting in safety checks. In C#7 we might have put safety checks on _engine and on _driver, even though conceptually _engine can never be null. Or we might have forgotten to put checks on either.

In C#8, the compiler would give us a warning on _driver, but would be happy with _engine, since it knows _engine can never be null. It tells us precisely where we need to put the checks:

if (_driver != null)
return $"Car with capacity {_engine.Capacity}cc is driven by {_driver.Name}.";
return $"Car with capacity {_engine.Capacity}cc has no driver.";

or perhaps

return $"Car with capacity {_engine.Capacity}cc is driven by {_driver?.Name ?? "no one"}.";

Either approach will remove the warnings. The compiler has enforced that a non-nullable reference cannot be assigned with null, so it doesn't insist that we check for null.

It's Complicated

If you start using nullable references, then you should never get a NullReferenceException, whilst also avoiding unnecessary checks for null.

But because the feature fundamentally changes the way that C# has be written for decades, there's a lot of further complexity, including attributes such as [NotNullWhen], and the intriguingly named Null-Forgiving Operator (a new use of the exclamation mark).

You can find out more in these two videos:


Enhance your programming skills further with one of our software design and development courses.


This piece was originally posted April 7, 2021 and has been reposted with updated formatting.