}

C# Nullable References

2021-04-07

The Biggest New Thing in C# for Years


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}.";
else
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:

Written by Dan Buskirk

The pleasures of the table belong to all ages.” Actually, Brillat-Savaron was talking about the dinner table, but the quote applies equally well to Dan’s other big interest, tables of data. Dan has worked with Microsoft Excel since the Dark Ages and has utilized SQL Server since Windows NT first became available to developers as a beta (it was 32 bits! wow!). Since then, Dan has helped corporations and government agencies gather, store, and analyze data and has also taught and mentored their teams using the Microsoft Business Intelligence Stack to impose order on chaos. Dan has taught Learning Tree in Learning Tree’s SQL Server & Microsoft Office curriculums for over 14 years. In addition to his professional data and analysis work, Dan is a proponent of functional programming techniques in general, especially Microsoft’s new .NET functional language F#. Dan enjoys speaking at .NET and F# user’s groups on these topics.

Chat With Us