DEV Community

Rex
Rex

Posted on • Edited on

Reduce Complexity with Nullable Reference Types

Introduction

C# 8.0 introduced the feature Nullable Reference Types(NRT), differentiating reference types and nullable reference types and giving us the ability to explicitly mark a reference type as nullable, offering compile-time static null-state analysis. (Nullable reference types | Microsoft Docs),

I spoke to families and friends about NRT and I have three questions:

  1. We are very confident about our TDD-produced codebase, and with extensive integration tests, all the use-cases are guarded by our tests, what benefits does the NRT offer?

  2. We have extensive validation rules, why should we care about NRT?

  3. With NRT, I am often forced to write very fat constructors, I hate it because it reduces readability and readability is the number one priority! Why is NRT more important than readability?

In this blog post, I attempt to answer the above questions.

The short answer has two parts:

  1. Requirement: NRT requires us to be mindful and explicit about whether a property can be null or not. Without this mind shift, NRT will only confuse everyone.

  2. Benefit: Assuming we are explicit about all reference types’ nullability, the static null-state analysis offered by the compiler will effectively increase the readability of our codebase and avoid throwing NullReferenceException at runtime without us paying much attention. It also promotes better encapsulation and simpler code. As a result, improves our development experience.

TDD and Nullability

TDD is about testing Input/output-based business requirements, focusing on the business functionalities, and avoiding testing implementation details at all costs.

TDD should pick up some cases and result in adding validation rules to guard against user inputs. However, TDD does not prevent NullReferenceException in our implementation details because they do not test implementation details.

Yes, if we write the correct integration tests, this will be picked up, but there is a big if here.

With NRT enabled and properties correctly marked as nullable, we can easily iron out most of NullReferenceException compile time.

Validation Rules and NRT

Validation rules are necessary to guard against public APIs and user inputs, we will always need to have them with or without NRT because we have no control over actions that happened outside.

However, we can control ourselves not to violate the NRT rules set for ourselves, we can rely on the compiler to eliminate the NullReferenceException. So writing validation rules to guard the internals for NullReferenceException is cluttering our codebase and increasing the complexity unnecessarily.

Readability and Better Encapsulation

When a class object has many required properties, with NRT enabled, the compiler would suggest generating a constructor to initialize the required properties. We would have a very fat constructor with way too many parameters, and the readability would be greatly reduced.

We should enforce the required properties in the constructor and ensure they are always valid(not null). It reflects my favorite definition out there for Encapsulation: An encapsulated object is an object that guarantees always stay in a valid state, a black box.

But readability should always be on the top of the priorities! That’s what record type is for, together with named parameters, we can enjoy the same readability.

With the Record type, we remove the need for a fat constructor by using the record primary constructor. For example, the below class on on the top becomes a record type that followed.

public class Person
{
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public string FirstName { get; init; }
    public string LastName { get; init; }
    public string? FavoriteBook { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
public record Person(string FirstName , string LastName){
    public string? FavoriteBook { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

And with the use of named parameters shown below, we can enjoy the same level of readability compared to the syntax we use and love as shown below without NRT and constructor enforcement. The beauty is that order doesn’t matter with the named parameters.

//The world without NRT and constructor:
var rex = new Person{
  FirstName = "Rex"
  LastName = "Ye",
  FavoriteBook = "The Godfather"
};
Enter fullscreen mode Exit fullscreen mode
// With named paramters (not much different than the code without NRT and constructor followed)
var rex = new Person(
  FirstName: "Rex"
  LastName: "Ye"
){
  FavoriteBook = "The Godfather";
};
Enter fullscreen mode Exit fullscreen mode

C# 11 may release a “required” keyboard. With it, constructors are no longer needed, it also enable us to use object initialiser.

Conclusion: NRT Reduces Complexity

When we are explicit about the nullability of our reference types, we are effectively telling the compiler what can be null and what cannot be. Most of the time, with the later versions of c#, the compiler will help us by issuing warnings about nullability and help us eliminate most of runtime NullReferenceExceptions.

With NRT, assuming that we do not violate the NRT rules intentionally(for example giving null! to the required parameters), we can safely avoid checking for nulls or writing validation rules for our internals, reducing the complexity of our codebase.

With NRT enabled everywhere and warnings dealt with, we would see a positive ripple effect on our development experience.

We still need to put the null checks and validation rules for our public APIs because we cannot control what happens outside of our codebase.

Limitations

  1. There are times the compiler issues false positives. When we are sure that a property cannot be null, we can safely use ! to suppress the warnings. Please use ! operators with care.

  2. There are a few Known Pitfalls we need to keep in mind.

Top comments (0)