I’m sure you’re well aware how the null
value is responsible for loads of extra, uninspiring code, and tons and tons of exceptions and crashes. Tony Hoare came up with this “innovation” in 1965. He later apologized for what he calls a billion dollar mistake.
Here’s an example of the extra null
checking we need to do. What we really want is that requiredField
can never be null
, but we have no way to make the compiler enforce that. Hence, we need to do the checking ourselves.
You might find it pointless to simply substitute one exception (NullReferenceException
) with another (ArgumentNullException
), but it really isn’t. A NullReferenceException
can emerge from deep within your code, without any hint about what shouldn’t have been null
, forcing you to sit down and inspect or debug the code in order to find out what’s wrong. If you’re lucky you have symbol files with a reference to the line number where the exception was thrown, but in production code the line numbers are often wrong due to optimized code, or the symbol files are missing altogether. An ArgumentNullException
is much more helpful, because it will refer to the problematic argument and hopefully also provide a meaningful error message.
Unfortunately, it’s often not possible to know whether a reference can be null
. That leaves us with the choice of sprinkling null
checks all over the code, or just making vague assumptions that some references will never be null
. Here’s an example of the former:
As professionals, perhaps we’re expected to check all code like this? Fine, but in many cases this extra code actually is superfluous. It is quite possible that _someDependency
, Foo
and Bar
can never be null
. But we’re not getting any hints from the compiler about that. And even if we inspect the code manually and conclude that this is the case, things may change in the future if SomeDependency
is modified.
What a mess this is! I’m surprised developers have accepted this state of affairs for so long without rioting! It’s certainly easy to see why this is called a billion dollar mistake. In fact, that’s likely to be an understatement.
Sadly, most programming languages have inherited null
, but languages like Haskell, OCaml, Scala, F#, Elm, and Rust have chosen to make use of option types as a robust alternative.
Option types is an elegant solution as long as it has been designed into the language to begin with. Retrofitting it into languages with null
is problematic, because now there are two ways to represent missing vales. You will still have to check for null
when dealing with legacy code. Nevertheless, this is the approach taken with Java 8 and the new Optional
type.
In contrast, Kotlin has managed to embrace null
as part of the language in a way that is both safe and pragmatic. How is that possible? The solution is actually quite straightforward when we focus on what we actually want to achieve: that it should not be possible to dereference a null
reference. All right, so all references need to explicitly be one of the following:
- Non-nullable
- Nullable, and thus not possible to dereference without a
null
check
In a way, this has been available to C# developers for ten years already, using the ReSharper Annotation Framework.
By annotating the code with attributes, ReSharper can help us where the C# type system falls short:
If I now try to set requiredField
to null
, ReSharper will catch it:
I will get a similar warning if I try to dereference optionalField
without checking for null
:
This is really useful, but far from perfect. First, these are just helpful hints and not compile errors. Second, they rely on a third-party tool. Third, you’re not forced to annotate all the code, so the problem never really goes away. And ReSharper defaults to an optimistic analysis, meaning that when annotations are missing, avoiding false alarms is deemed more important than pointing out all potential code issues. Fourth, all these annotations aren’t exactly making the code any prettier.
Luckily, we no longer have to concern ourselves with any of that, because C# 8 comes with nullable reference types built-in. The naming is a bit confusing, because reference types have always been nullable, and that’s the whole problem. The novelty is that they can now also be non-nullable. The reason for the name is that reference types can now be non-nullable by default, or explicitly nullable using Nullable<T>
or T?
. For backward compatibility, you need to enable the feature in the code or project files. Also, any issues detected by the compiler are just warnings by default, so you may want to treat warnings as errors.
Here’s the same class as before, but note how much cleaner this looks without the extra attributes:
If I try to set requiredField
to null
, I get a similar message as before, but this time from the compiler itself:
Naturally, it also catches a dereferencing of optionalField
:
Needless to say, I think we should all enable this feature for any new code1, using the following project file settings:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
<NullableReferenceTypes>true</NullableReferenceTypes>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
How about existing code? Should we go ahead and enable nullable reference types there too? Not necessarily, because going through all the compiler errors and fixing them is a significant job, and there is always the risk that this in itself will introduce new bugs. If you are motivated to try, see here and here for a taste of what to expect.
-
This is under the assumption that you’re working on application code where you can enable C# 8 everywhere. If you’re building a library and expect some clients to use older versions of C#, or to simply ignore the new compile warnings, you need to consider adding the extra
null
checking as before. More about this here. ↩