.NET Tools
Essential productivity kit for .NET and game developers
Null pointers: an opportunity, not an exception – Code smells series
This post is part of a 10-week series by Dino Esposito (@despos) around a common theme: code smells and code structure.
Last week, we looked more closely at using the classical object-oriented concept of inheritance in our code base. This week, we will look at null and how it presents us with an opportunity to improve our code.
In this series:
- Sharpen your sense of (code) smell
- The simple case of special string types
- Join data items that want to go together
- Easy conversions and readability
- Put it down in layman’s terms
- Every method begins with “new”
- Dependency injection doesn’t strictly require frameworks
- Super SuperClasses
- Null pointers: an opportunity, not an exception
- You ain’t gonna use it!
As inconsistent and immaterial as it may appear, the value null
is not a value, and yet many programming languages insist on treating as if it were. To survive, C# developers have to repeatedly check references to their classes for existence, though Java developers don’t fare much better.
Null checking is boring, and without a doubt every developer will find a way to forget one. It seems that the concept of null references exists thanks to Sir Tony Hoare, a prominent computer scientist, inventor of the Quicksort algorithm and the winner of the Turing award in 1980. The concept of null was introduced in the Algol language, and later Sir Hoare called it “my billion-dollar mistake”. It is nice to recall how the Algol language was presented to the masses back in the 1960s.
Here is a language so far ahead of its time, that it was not only an improvement on its predecessors, but also on nearly all its successors.
The main issue with null references is that once the language allows references to be null, well, any reference can potentially be null. Hence, null checking is mandatory. But boring. And developers tend to forget it. And programs crash because of that.
For a number of practical and architectural reasons, there’s not much that established programming languages like C# and Java can do other than introducing contrivance and helper operators. New languages, however, free of building a type system from scratch, can do more. And Kotlin, in effect, did. In Kotlin there’s a clear distinction between nullable and non-nullable types. If you assign a null
value to a non-nullable type you get a compilation error.
var text: String = "abc" text = null // compile error
In C#, things are slightly different. Nullability is an exclusive attribute of value types, and any reference type can always take null as a value.
string text = "abc"; text = null;
In Kotlin, instead, any type is non-nullable unless it is explicitly made nullable. Kotlin also provides a syntax for safe calls that don’t throw a Null-Reference exception but apply some compensation logic that ultimately results in a no-op and a null value being returned.
In the code snippet below, the contact
is passed an empty phone number and the use of the ?.
safe call operator actually sets the phoneNumber
variable to null, without any sort of exception.
val customer: Contact? = Contact(Email("..."), Phone(null)) val phoneNumber = customer?.phone?.number
The safe call operator, or safe navigation operator, is also available in C# since version 6.0, but it doesn’t work exactly like the Kotlin operator. While Kotlin proceeds along the entire chain, the C# safe navigation operator stops at the first null reference encountered.
Safe call operators are one way to deal with null references to avoid annoying exceptions in C# classes. ReSharper also does a lot of work in order to detect possible occurrences of null-reference exceptions, as shown below. Yet the original sin – the billion-dollar mistake – remains.
The root of the problem is that null
is not a value and it can’t refer to any domain-specific state. In any business ubiquitous language, there’s nothing like null
, a non-value. There are, instead, special case values that can be assimilated to what in raw computer memory terms is a null reference. The Special Case pattern (in many places labeled as the Null Object pattern) suggests that you never use null values. And whenever you’re tempted to do so, you return an instance of a special class (often a singleton) that represents an invalid or inconsistent state of the logical entity behind the class. Here’s an example:
public class MissingContact : Contact { public static MissingContact Instance = new MissingContact(); private MissingContact() { } }
A class that returns a Contact
object will return a MissingContact
instance instead of null
or other odd values that may denote a special circumstance or an invalid state.
public Contact Find(string id) { // Id starting with "1" are invalid for some reason if (id.StartsWith("1")) return MissingContact.Instance; // More options ... return new Contact(); }
A similar approach may lead to a proliferation of small and similar looking classes: one for each possible exceptional situation to handle. One way to work around the problem is using a single MissingContact
class with a few parameters that can be set to whatever is helpful to figure out the special case. This could be error codes, messages, or exceptions.
The key to the success of the Special Case pattern is that the caller receives an instance of the same type it was expecting.
Now, what if something really weird and unexpected occurs? There might be cases (or just personal development preferences) in which you just want a call to fail. In these scenarios, the use of null
as return values is better replaced by using specific exceptions.
// Really weird situation ... need to fail. if (id.StartsWith("0")) throw new WeirdContactException();
In the end, null
is a problem because it’s not a value but is perceived as such (kudos to Sir Tony Hoare for recognizing it so lucidly). At the same time, fixing the use of null
in your code also represents a great opportunity to improve and refactor for better readability and a lesser chance of that dreaded NullReferenceException
.
There’s just one week left in our series, so let’s prepare to yell an acronym when overengineering peeks around the corner. YAGUI! (Yes, that is indeed very close to YAGNI.)
Download ReSharper 2018.1.4 or Rider 2018.1.4 and give them a try. They can help spot and fix common code smells! Check our code analysis series for more tips and tricks on automatic code inspection with ReSharper and Rider.