Into

Null handling and null checks can be quite a hot topic for some developers. On one hand, it can be used as a default “no value yet” initializer for variables, and is easy to check if said variable holds a value or not. But on the other hand, it can lead to a bloated codebase, where you constantly need to check for null.

This blog post is not meant as an attack on null. It’s meant to shine a light on how you can develop a codebase without littereing null checks everywhere, and how it’s absence can clean up the codebase.

Notice, this blogpost is written from a C# standpoint. Other languages might not handle null in the same way.

Returning Null

Often when we create methods, it’s tempting to return a null if the code fails or don’t have the correct value. Notice this example:

public string? GetValue(string key)
{
    return _db.StringGet(key);
}

We return either null, or the value from out database. There is always a tradeoff when dealing with null. When we have to itterator over a value, or store a value, it uses less code to not have to deal with null. When we have to mutate or handle values where we are not always certain, that the value is correct or not, we sometimes need to check for null. In the example above, we’re not certain that we will get anything out of the database, therefor we just return null. If it’s null, we return null. And if we get an actual value, we return that value.


Now observe the result of what having to handle null will end up with. This code might look fine at first. We have an input field where we place a key, a paragraph tag where our result will be shown, and a button to get the result. We bind the input to the key value, and access the value in the paragraph. We have a method to return the value of the key, once the button is pressed. Notice that our SingleKeyValuePair can be null.

<input type="text" @bind="SingleKeyValuePair.Key" placeholder="Key"/>
<p>@SingleKeyValuePair.Value</p>
<button @onclick="GetSingleKeyValuePair">Get single key value pair</button>

@code
{
    private KeyValueModel? SingleKeyValuePair { get; set; } = new();

    private void GetSingleKeyValuePair()
    {
        SingleKeyValuePair.Value = RedisHandler.GetValue(SingleKeyValuePair.Key);
    }
}

That’s not really a problem normaly. But what is a problem is strict codebases where warnings is seen as errors, and the project refuses to build. The input tag and our GetSingleKeyValuePair() method is raising a Dereference of a possibly null reference warning. And should we get a null value from the GetValue(string key) method, our project would crash, since there is nothing handling the null value.

But we can fix these warnings with null checks like so:

@if (SingleKeyValuePair is not null)
{
    <input type="text" @bind="SingleKeyValuePair.Key" placeholder="Key"/>
}
else
{
    SingleKeyValuePair = new();
    <input type="text" @bind="SingleKeyValuePair.Key" placeholder="Key"/>
}

@code
{
    private void GetSingleKeyValuePair()
    {
        if (SingleKeyValuePair is null)
        {
            return;
        }
        SingleKeyValuePair.Value = RedisHandler.GetValue(SingleKeyValuePair.Key) ?? string.Empty;
    }
}

Now we don’t have any warnings, but we just added 11 lines just to account for 2 null warnings. We can do better than this, and let me show you how.

First we handle null checks in our GetValue() method. If the database returns null, we return an empty string. It’s one extra line of code.

public string GetValue(string key)
{
    string? value = _db.StringGet(key);
    return value ?? "";
}

Then we remove nullability from our SingleKeyValuePair model. And of cause remove all of the null handling surrounding that variable, since we either have a value or an empty sting. No nulls to check or handle.

The resulting code now looks like this again, but without null:

<input type="text" @bind="SingleKeyValuePair.Key" placeholder="Key"/>
<p>@SingleKeyValuePair.Value</p>
<button @onclick="GetSingleKeyValuePair">Get single key value pair</button>

@code
{
    private KeyValueModel SingleKeyValuePair { get; set; } = new();

    private void GetSingleKeyValuePair()
    {
        SingleKeyValuePair.Value = RedisHandler.GetValue(SingleKeyValuePair.Key);
    }
}

There you have it ladies and gentlemen. We have come full circle. We introduce one line of code to either return the value from out database, or an empty string, if there is no value in the database. And that one line saved us 11 lines of null handling in out UI code.

Performance

Now, it wouldn’t be a fullfilling blogpost for me if I didn’t talk about the performance impact of returning empty strings compared to null. Or maybe even returning empty lists.

This test is pretty simple. I have 4 methods.

  • 1st method returns null string
  • 2nd returns an empty string
  • 3rd returns an empty string list
  • 4th returns an empty int list
  • 5th returns 0
  • 6th returns a very big struct

With that, we have a pretty wide range of return objects of varius sizes. I will of cause use the BenchmarkDotNet library to do the benchmarking.

Method Mean Error StdDev Median Allocated
ReturnInt 0.0038 ns 0.0069 ns 0.0064 ns 0.0000 ns -
ReturnNullString 0.0188 ns 0.0286 ns 0.0268 ns 0.0041 ns -
ReturnEmptyString 0.5157 ns 0.0154 ns 0.0136 ns 0.5110 ns -
ReturnIntList 9.8375 ns 0.1163 ns 0.1087 ns 9.8120 ns 32 B
ReturnStringList 12.1541 ns 0.0572 ns 0.0477 ns 12.1489 ns 32 B
ReturnBigStruct 26.3674 ns 0.0890 ns 0.0832 ns 26.3507 ns -

Conclusion

And there we have it. Returning null is almost the fastest, with returning an empty string third. But one thing to keep in mind, this is nano seconds we’re talking about. That is 0.0000000005157 seconds to return an empty string. At this scale, I think it fine returning an empty string instead of a null.

But at the end of the day, having null in a codebase is inevitable. We do need null handling, but the place you do the null handling is more important in my opinion.

Test Code

And the code used:

MyBenchmarks.cs

[MemoryDiagnoser]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class MyBenchmarks
{
    [Benchmark]
    public string? ReturnNullString()
    {
        return null;
    }
    [Benchmark]
    public string ReturnEmptyString()
    {
        return "";
    }
    [Benchmark]
    public List<string> ReturnStringList()
    {
        return new List<string>();
    }
    [Benchmark]
    public List<int> ReturnIntList()
    {
        return new List<int>();
    }
    [Benchmark]
    public int ReturnInt()
    {
        return 0;
    }
    
    [Benchmark]
    public BigStruct ReturnBigStruct()
    {
        return new BigStruct();
    }
}

Program.cs

var summary = BenchmarkRunner.Run(typeof(Program).Assembly);