Friday, July 15, 2022

Nullability in C# - What It Is and What It Is Not

Starting with .NET 6, new projects have nullable reference types enabled by default. It is easy to get confused on exactly what that means, particularly when migrating existing projects. Today, we'll take a look at what nullability is and what it isn't. In future articles, we'll look at the null operators in C# (null conditional, null coalescing, and null forgiving) -- these are all various combinations of "?", "!", and ".".

Articles

The source code for this article series can be found on GitHub: https://github.com/jeremybytes/nullability-in-csharp.

The Short Version

Nullability Is:
  • A way to get compile-time warnings about possible null references
  • A way to make the intent of your code more clear to other developers
Nullability Is NOT:
  • A way to prevent null reference exceptions at runtime
  • A way to prevent someone from passing a null to your method or assigning a null to an object

Read on for details and examples.

Getting a Null Reference Exception at Runtime

Before looking at what nullability gives us, let's take a look at a project that does not have nullability enabled. This can be found at the GitHub repository noted above:  https://github.com/jeremybytes/nullability-in-csharp. (See the README file of on the repository for information on how to run the application yourself.)

The "StartingCode" folder contains a set of projects where nullability is not enabled. Here is a screenshot of the running application (the "UsingTask.UI" project in the solution):




And here is the code hooked up to to the "Cancel" button at the bottom (specifically from the MainWindow.xaml.cs file in the "UsingTask.UI" project):


    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        tokenSource.Cancel();
    }

The problem with the code is that the "tokenSource" field in this code may be null. (We won't go into the details of why that may be true; if you want more information, you can look at the resources about Task and Cancellation here: https://github.com/jeremybytes/using-task-dotnet6.)

If we run the application and then immediately click the "Cancel" button (without clicking either of the other buttons first), we get a runtime error -- a Null Reference Exception:


System.NullReferenceException: "Object reference not set to an instance of an object."

This is because we are trying to call the "Cancel" method on a null tokenSource.

Nullability and nullable reference types are there to help us prevent these types of errors. So let's enable nullability and see what we help we get (and what help we do not get).

Enabling Nullable Reference Types

As mentioned above, nullability is enabled by default when you create a project with .NET 6. Nullability can also be enabled by editing the .csproj file for projects that are upgraded from .NET 5 or .NET Core.

To enable nullable reference types, set the "Nullable" property to enable in the .csproj file. Here is an excerpt from the UsingTask.UI.csproj file (note the code in the "StartingCode" folder has this commented out; the "FinishedCode" has it uncommented):


    <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net6.0-windows</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <UseWPF>true</UseWPF>
    </PropertyGroup>

Note: I also made this change to the UsingTask.Library.csproj file (but we will not look at this project until a later article).

Now that nullability is enabled, let's take a look at what this means for the project.

With this property set, all reference types (whether they used as fields, properties, method arguments, return types, or in other ways) are assumed to be non-nullable. To make a reference type nullable, we need to explicitly state that (and we will see how in just a bit).

Nullability Is: A way to get compile-time warnings about possible null references

The first thing we can do is see what messages we get. If we re-build the solution, some green squigglies will show up to alert us to possible problems. For example, there is now a warning on on the constructor for the MainWindow class:


    CancellationTokenSource tokenSource;

    public MainWindow()
    {
        InitializeComponent();
    }

If we hover over the warning, we can get the details:


CS8618: Non-nullable field 'tokenSource' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

This message tells us that the "tokenSource" field will be null when the constructor exits (and so the field will be null).

Another way is to open the "Error List" in Visual Studio to see each of the warnings listed.


This includes the message above as well as 2 others (we will explore these in later articles).

So Nullability is a way to get compile-time warnings about possible null references.

Nullability Is NOT: A way to prevent null reference exceptions at runtime

Although we get the warnings (and that helps us), this does not prevent us from building and running the application.

If we build and run the application again, and then click the "Cancel" button, we get the same Null Reference Exception that we got before:


System.NullReferenceException: "Object reference not set to an instance of an object."

Even though the "tokenSource" field is non-nullable, this is not enforced at runtime. Since we do not assign a value to the "tokenSource" in the constructor, it is null when we use it in the Cancel button's event handler.

So Nullability is not a way to prevent null reference exceptions at runtime.

This is important to keep in mind when we are working with nullability. It is helpful to us at compile-time, but it does not have a runtime effect.

Nullability Is NOT: A way to prevent someone from passing a null to your method or assigning a null to an object

As we saw above, these are compiler warnings (and those warnings are useful). But they do not prevent someone from assigning "null" to an object or passing "null" as an argument to a method.

As a very contrived example, we can assign a "null" to the "tokenSource" field in the constructor:


    CancellationTokenSource tokenSource;

    public MainWindow()
    {
        InitializeComponent();
        tokenSource = null;
    }

We do get a warning:


CS8625: Cannot convert null literal to non-nullable reference type.

And the warning is useful. If we have our compiler set to fail on warnings (which is used by many teams), then it would stop this code from compiling. But there is nothing that forces another developer to have "fail on warnings" turned on. The code is still buildable and runnable.

More subtly, if we are building a library that is used by another project, that project could pass a null to one of our library methods (even if we have it specified as non-nullable). So it is still very important that we check for nulls in our code. We'll dive into this a little deeper in a subsequent article.

So Nullability is not a way to prevent someone from passing a null to your method or assigning a null to an object.

Nullability Is: A way to make the intent of your code more clear to other developers

One key feature of nullable reference types is that it can make the intent of your code more clear. When we have nullability enabled in our code, all reference types are non-nullable by default. If we want to have a field or variable that is nullable, we need to mark it as such using "?".

The "tokenSource" field should be nullable. To let other developers (and the compiler) know about this, we add a question mark at the end of the type when we declare the field:


    CancellationTokenSource? tokenSource;

    public MainWindow()
    {
        InitializeComponent();
    }

This means that the "tokenSource" field is allowed to be null, and the warning that we got on the constructor is now gone.

But now that the "tokenSource" field is nullable, we get a different warning in the Cancel button's event handler:


    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        tokenSource.Cancel();
    }

Here are the message details:



'tokenSource' may be null here.
CS8602: Dereference of a possibly null reference.

This tells us that we have a potential for a null reference exception at runtime (and we have seen that happen several times already).

When we come across a message like this, we should have a guard clause that does a null check. The way that we used to do this is to wrap the code in an "if" statement:


    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        if (tokenSource is not null)
        {
            tokenSource.Cancel();
        }
    }

This code makes sure that "tokenSource" is not null. If it is null, then it skips over the code and does nothing. Otherwise, it will run the "Cancel" method. This prevents the null reference exception at runtime.



Even if we click the "Cancel" button immediately after starting the application, we do not get a null reference exception. This is because we have a guard clause to prevent that.

The code above works, but there is an easier way of doing this with the null conditional operator:


    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        tokenSource?.Cancel();
    }

We won't go into the details of the null conditional operator right now; that is the subject of the next article.

Wrap Up

So we have seen that nullability and nullable reference types can be very useful for sharing the intent of our code. It can also help us find potential null reference exceptions in our application. But it does not stop us from compiling, and it does not stop null reference exceptions from happening at runtime.

Nullability Is:
  • A way to get compile-time warnings about possible null references
  • A way to make the intent of your code more clear to other developers
Nullability Is NOT:
  • A way to prevent null reference exceptions at runtime
  • A way to prevent someone from passing a null to your method or assigning a null to an object
The usefulness is very good, and it can save us a lot of time hunting down bugs in our code. But we do need to be careful not to rely on it too heavily.


Happy Coding!

1 comment:

  1. Thanks for these great articles and thanks for not having Ads everywhere!

    ReplyDelete