Functional C#: Non-nullable reference types

The topic described in this article is a part of my Applying Functional Principles in C# Pluralsight course.

This is the third article in my Functional C# series.

C# non-nullable reference types: state of affairs

Look at the code example below:

Customer customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

Looks pretty familiar, doesn’t it? But what issues do you see in this code?

The problem here is that we don’t know for sure whether or not GetById method can return null. If there is any chance for it to return null, then we’ll be getting NullReferenceException at run-time. Even worse, there could pass a significant amount of time between getting the customer instance and using it. Therefore, the exception we get will be hard to debug because we won’t be able to easily find out where exactly the customer instance turned to null.

The faster we receive feedback, the less time it takes us to fix the problem. Of course, the quickest feedback possible can only be given by compiler. How cool would it be to just write this code and let the compiler do all the checks required?

Customer! customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

Where Customer! stands for non-nullable type, i.e. a type whose instances can’t turn into null in any way. How cool would it be to be sure that the compiler will tell you if there are any possible code paths that return null?

Yep, very cool. Or even better:

Customer customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

That is, make all the reference types non-nullable by default (just as value types are) and if we want to introduce a nullable type then put it this way:

Customer? customer = _repository.GetById(id);
Console.WriteLine(customer.Name);

Can you imagine a world without all those annoying null reference exceptions? Me neither.

Unfortunately, non-nullable reference types can’t be introduced in C# as a language feature. Such design decisions should be implemented from the day one because otherwise they break almost every existing code base. Check out these articles to get to know more about this topic: Eric Lippert’s article, interesting but probably not realizable design proposal.

But don’t worry. Although we can’t make compiler help us on our way to leverage the power of non-nullable reference types, there still are some workarounds we can resort to. Let’s look at the Customer class we ended up with in the previous post:

public class Customer
{
    public CustomerName Name { get; private set; }
    public Email Email { get; private set; }
 
    public Customer(CustomerName name, Email email)
    {
        if (name == null)
            throw new ArgumentNullException("name");
        if (email == null)
            throw new ArgumentNullException("email");
 
        Name = name;
        Email = email;
    }
 
    public void ChangeName(CustomerName name)
    {
        if (name == null)
            throw new ArgumentNullException("name");
 
        Name = name;
    }
 
    public void ChangeEmail(Email email)
    {
        if (email == null)
            throw new ArgumentNullException("email");
 
        Email = email;
    }
}

We moved all the email and customer name validations to separate classes, but we couldn’t do anything with the null checks. As you can see, they are the only checks remaining.

Getting rid of the null checks

So, how can we get rid of them?

By using an IL rewriter, of course! There’s a great NuGet package named NullGuard.Fody built exactly for that purpose: it weaves your assemblies with null checks all over your code base making your classes throw an exception in case a null value comes in as a parameter or comes out as a method result.

To start using it, install the package NullGuard.Fody and mark your assembly with this attribute:

[assembly: NullGuard(ValidationFlags.All)]

From now, every method and property in the assembly automatically gets a null validation check for any input parameter or output value. Our Customer class can now be written as simply as this:

public class Customer
{
    public CustomerName Name { get; private set; }
    public Email Email { get; private set; }
 
    public Customer(CustomerName name, Email email)
    {
        Name = name;
        Email = email;
    }
 
    public void ChangeName(CustomerName name)
    {
        Name = name;
    }
 
    public void ChangeEmail(Email email)
    {
        Email = email;
    }
}

Or even simpler:

public class Customer
{
    public CustomerName Name { get; set; }
    public Email Email { get; set; }
 
    public Customer(CustomerName name, Email email)
    {
        Name = name;
        Email = email;
    }
}

This is what gets compiled under the hood:

public class Customer
{
    private CustomerName _name;
    public CustomerName Name
    {
        get
        {
            CustomerName customerName = _name;
 
            if (customerName == null)
                throw new InvalidOperationException();
 
            return customerName;
        }
        set
        {
            if (value == null)
                throw new ArgumentNullException();
 
            _name = value;
        }
    }
 
    private Email_email;
    public Email Email
    {
        get
        {
            Email email = _email;
 
            if (email == null)
                throw new InvalidOperationException();
 
            return email;
        }
        set
        {
            if (value == null)
                throw new ArgumentNullException();
 
            _email = value;
        }
    }
 
    public Customer(CustomerName name, Email email)
    {
        if (name == null)
            throw new ArgumentNullException("name", "[NullGuard] name is null.");
        if (email == null)
            throw new ArgumentNullException("email", "[NullGuard] email is null.");
 
        Name = name;
        Email = email;
    }
}

As you can see, the validations are equivalent to the ones we wrote manually except that there are also validations for return values which is a good thing.

How to introduce a null value?

So how do we state that a value of some type can be null? We need to use Maybe monad:

public struct Maybe<T>
{
    private readonlyT _value;
 
    public T Value
    {
        get
        {
            Contracts.Require(HasValue);
 
            return _value;
        }
    }
 
    public bool HasValue
    {
        get { return _value != null; }
    }
 
    public bool HasNoValue
    {
        get { return !HasValue; }
    }
 
    private Maybe([AllowNull] T value)
    {
        _value = value;
    }
 
    public static implicitoperatorMaybe<T>([AllowNull] T value)
    {
        return new Maybe<T>(value);
    }
}

As you can see, the input values for the Maybe class are marked with AllowNull attribute. That tells our null guard weaver that it shouldn’t emit null checks for these particular parameters.

With Maybe, we can write the following code:

Maybe<Customer> customer = _repository.GetById(id);

And it now becomes obvious that the GetById method can return a null value. From now, we can reason about the code without stepping into it!

Moreover, you now can’t accidentally mess up a nullable value with a non-nullable one, that would lead to a compiler error:

Maybe<Customer> customer = _repository.GetById(id);
ProcessCustomer(customer);// Compiler error
private void ProcessCustomer(Customer customer)
{
    // Method body
}

Of course, you need to consciously decide what assemblies should be weaved. It probably won’t be a good idea to enforce those rules in WFP presentation layer as there are a lot of system components that are inherently nullable. In such environment, null checks just won’t add any value because you can’t do anything with those nulls.

As for domain assemblies, it totally makes sense to introduce such enhancement for them. Moreover, they would benefit from such approach the most.

One little note about Maybe monad. You would probably want to name it Option because of F# language naming conventions. I personally prefer to entitle it Maybe but I would say there is 50/50 distribution between programmers who name it Maybe and the ones who prefer Option name. Of course, it’s just a matter of taste.

What about static checks?

Okay, fast feedback in run-time is good but it is still only a run-time feedback. It would be great if there would be a way to somehow analyze the code statically and provide the feedback even faster, at compile time.

There is such a way: Resharper’s Code Annotations. You can use NotNull attribute to mark method’s parameters and return values as non-nullable. That allows Resharper to raise a warning if you pass a null to a method which parameters are not allowed to be null.

While such approach can be a pretty helpful assistant, it suffers from several problems.

First of all, in order to state that a parameter can’t be null, you should take an action, i.e. mark it with an attribute. It would be better to apply a reversed technique: mark a parameter only if you want it to be nullable. In other words, make non-nullability default and opt-out parameters if necessary just as we do with NullGuard.

Secondly, warning is only a warning. Of course, we could set up "warning as an error" settings in Visual Studio, but still, use of Maybe monad leaves much less wiggle room for potential bugs as it prevents us from illegal use of non-nullable types.

That’s why, although Code Annotations are very useful in some cases, I personally tend to not using them.

Summary

Approach described above is a really powerful one.

  • It helps to reduce the amount of bugs by providing fast feedback if a null value unexpectedly sneaks in.

  • It significantly increases code readability. You don’t need to step into the method to find out whether or not it can return a null value.

  • The null checks are there by default meaning that all of your methods and properties are null-safe unless you specify otherwise. It makes code much cleaner as you don’t need to specify NotNull attributes all over your code base.

Next time, we’ll discuss how to handle exceptions in a functional way. Stay tuned.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus