Wednesday, July 20, 2022

Null Coalescing Operators in C# - ?? and ??=

Over the last few articles, we have looked at nullability in C#. When we have nullability enabled, we need to respond to warnings that come up in our code. Sometimes when we come across a null value, we would like to replace it with a default non-null value. This is something that the null coalescing operator (2 question marks) can help us with.

Articles

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

The Short Version

When we have a null value, we often want to replace it with a non-null default value. The null coalescing operator can help us with this.

Here's an example of a traditional way to write this code:


    if (result is null)
        return new List<Person>();
    return result;

With the null coalescing operator, we can reduce this to a single line that include the null check and the non-null default value:


    return result ?? new List<Person>();

If "result" is not null, "result" is returned. If "result" is null, a new empty list is returned.

Let's look at this in more detail.

Possible Null Return

In the last article (about the null forgiving operator), we looked at a possible null return value for a method. We will start with that same method. The code is available in the GitHub repository for this article series: https://github.com/jeremybytes/nullability-in-csharp.

The "StartingCode" folder has the starting state of the code at the beginning of the first article in the series. The "FinishedCode" has the completed code. The links in this article will point to the "FinishedCode" folder unless otherwise noted.

As a reminder, the "GetAsync" method needs to return a non-null value, but in the "StartingCode", we get a possible null warning. (This is in the "PersonReader.cs" file of the "UsingTask.UI" project.)


    public async Task<List<Person>> GetAsync(
        CancellationToken cancelToken = new CancellationToken())
    {
        [collapsed code]

        var result = JsonSerializer.Deserialize<List<Person>>(stringResult, options);
        return result;
    }
    'result' may be null here.
    CS8603: Possible null reference return.

The "GetAsync" method returns a "Task<List<Person>>". The "List<Person>" part is non-nullable.

But because of the way the JsonSerializer works, we may end up with a "null" in the "result" variable. Because of this, we get a warning that we have a "possible null reference return".

Using a Guard Clause
As we saw in earlier articles, the traditional way of dealing with possible nulls is to set up a guard clause that lets us handle things appropriately. We can fix this code with an "if" statement:


    if (result is null)
        return new List<Person>();
    return result;

In this code, we first check to see if "result" is null. If it is, then we return a new empty list. If "result" is not null, then we return "result" directly. (Since the first "return" exits out of the method, we do not need an "else" block. We will only hit the second return if the "if" evaluates to false.)

Just like with the null conditional operator that we saw previously, we can get rid of the explicit guard clause by using the null coalescing operator.

Null Coalescing Operator - ??

The null coalescing operator is 2 question marks. Here is the same code block using this operator:


    return result ?? new List<Person>();

Here is how this operator works. Whatever is on the left side of the "??" operator (in this case the "result" variable) is checked for a "null". If the value is not null, then that value is returned. If the value is null, then whatever is on the right side of the "??" operator (in this case "new List<Person>()") is returned.

This works when we want to assign a value to a variable or field as well as when we want to return a value. Let's look at another example that assigns the result to a field.

Property with a Default Value

For this example, we will go back to the "MainWindow.xaml.cs" file in the "UsingTask.UI" project.

Here is the code that we will start with (this block is specifically from the file in the "StartingCode" folder):


    public MainWindow()
    {
        InitializeComponent();
        reader = new PersonReader();
    }

    private PersonReader? reader;
    public PersonReader Reader
    {
        get => reader;
        set => reader = value;
    }

This code has a non-null "Reader" property that uses a nullable "reader" field. (We'll go into the reasoning behind this in just a bit.) The constructor initializes the "reader" field to a non-null value.

Even though the "reader" property is not null when the class is created, we still get a warning in the "Reader" getter because the backing field is nullable (and it could be set to null elsewhere in the code).


    'reader' may be null here.
    CS8603: Possible null reference return.

Moving the Property Initialization

As a first step, let's move the field initialization out of the constructor and put it into the property getter.

For this, we will use a traditional guard clause (from the "MainWindow.xaml.cs" file of the "UsingTask.UI" project in the "FinishedCode" folder).

We will walk through several steps to get to a final state. This will help us better understand the code that we end up with. If you look at the file in the "FinishedCode" folder, you will find each of the steps in comment blocks.


    public MainWindow()
    {
        InitializeComponent();
    }

    private PersonReader? reader;
    public PersonReader Reader
    {
        get
        {
            if (reader is null)
                reader = new PersonReader();
            return reader;
        }
        set => reader = value;
    }

The getter for the property first checks the backing field to see if it is null. If it is null, then it assigns a value to it and then returns it. This ensures that the "Reader" property will never return a null.

Why Would We Do This?
You're probably wondering why we would change the code this way. The difference is pretty subtle. With either of the blocks of code, the "reader" field gets initialized. The difference is when the field is initialized.

In the first example, the field is initialized in the constructor. So it is set when the class is instantiated.

In the second example, the field is initialized the first time the "Reader" property is accessed. This means that it may never get set (if we never use the "Reader" property).

One place I have used this pattern is with property injection. Property injection is a good way to have a default value for a dependency that you can still swap out for unit testing. You can read more about a specific scenario where I've used this pattern here: Property Injection: Simple vs. Safe. (Hint: It involves serial ports that I do not want to initialize unless I actually use them.)

So assuming this is a pattern that we want to use, let's move on to integrating the null coalescing operator.

Adding the Null Coalescing Operator

Just like with our earlier example, we can remove the guard clause by using the null coalescing operator:


    private PersonReader? reader;
    public PersonReader Reader
    {
        get
        {
            reader = reader ?? new PersonReader();
            return reader;
        }
        set => reader = value;
    }

This code uses the "??" operator to check whether the "reader" field is null. If it is not null, then "reader" gets assigned to itself (so no change). If it is null, then a "new PersonReader()" is assigned to the "reader" field.

Either way, the "reader" that is returned in the property getter will not be null.

Combining Operators - ??=

The next step is to combine operators. The title and introduction make it look like "??" and "??=" are 2 separate operators. But technically they are not. Let's take a step back to look at something more familiar.

To increment a value, we can use the following code:
    int a = 3;
    a = a + 2;
This uses the "+" operator to add 2 to "a" and then assign the result back to "a". The effect is that the "a" variable is increased by 2.

But we can combine the "+" operator with the "=" operator to create the following:
    int a = 3;
    a += 2;
This has the same effect: the "a" variable is increased by 2.

We can do the same thing with the "??" operator. We can combine the null coalescing operator (??) with the assignment operator (=) to get ??=.

Here's what that looks like in code:


    private PersonReader? reader;
    public PersonReader Reader
    {
        get
        {
            reader ??= new PersonReader();
            return reader;
        }
        set => reader = value;
    }

This will do the null check on the "reader" field. If it is not null, then the value of "reader" is unchanged. But if "reader" is null, then a new PersonReader is assigned to it.

So just like with a +=, we can use ??= to combine the null coalescing operator with the assignment operator.

Making Things Shorter

We can further condense the code by combining the assignment of the "reader" field with the return of that field. This is where the code may get a bit confusing (which is why we're going in small steps):


    private PersonReader? reader;
    public PersonReader Reader
    {
        get { return reader ??= new PersonReader(); }
        set => reader = value;
    }

In our earlier example, we used the null coalescing operator to decide to return "result" (if it was not null) or a new empty list (if it was null).


    return result ?? new List<Person>();

We could do this with the property getter, but it would not set the "reader" field. So if the "reader" field was null, it would stay that way.

By using the combined operator "??=" the "reader" field is checked for null and assigned a value if it is null. Then the value of "reader" is returned.

It does take a little getting used to.

    get { return reader ??= new PersonReader(); }

We just have to remember that "??=" does both a null check and a possible assignment.

One Last Step: Expression-Bodied Getter
As long as we are making things short, we can make this a bit smaller by using an expression-bodied member for the getter.

Here's what that code looks like:


    private PersonReader? reader;
    public PersonReader Reader
    {
        get => reader ??= new PersonReader();
        set => reader = value;
    }

Since our getter only has a single expression (meaning one line of code that returns a value), we can use the "=>" operator and remove the curly braces and "return" keyword.

Expression-bodied members are a bit outside the scope of this article. But hopefully this syntax will make more sense if you run into it in someone else's code.

Wrap Up

The null coalescing operator (??) is very useful in eliminating nulls from our code. We can check for a null and provide a non-null default value. This way, we know that the result will not be null.

In addition, we can combine the null coalescing operator with the assignment operator - ??=. This gives us the ability to check a variable or field for "null" and then assign a valid default to it instead.

When we enable nullability in our projects, Visual Studio gives us compile-time warnings to help us eliminate unintended nulls. We can use the various null operators (null conditional, null forgiving, and null coalescing) to address those warnings.

The result is that we have code that is less likely to throw null reference exceptions at runtime. (And that means less debugging!)

Be sure to check the GitHub repository for the code samples and links to all of the articles: https://github.com/jeremybytes/nullability-in-csharp.

Happy Coding!

Tuesday, July 19, 2022

Null Forgiving Operator in C# - !

Over the last couple articles, we have been looking at nullability in C#. When we have nullability enabled, we need to respond to warnings that come up in our code. Sometimes these warnings are incorrect (after all, the analyzers are not infallible). Fortunately for us, there is a null forgiving operator (represented by an exclamation point) that we can use to say that we want to ignore one of these incorrect warnings.

Articles

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

The Short Version

Sometimes the warnings that we get around nullability are incorrect. When we are confident that an object will not be null, we can use the null forgiving operator (also called the null suppressing operator) to remove the warning. We do this by using an exclamation point.

In the following example, the analyzer thinks that the "Exception" property on task may be null. We can tell the analyzer that we know it is not null by using the null forgiving operator.


    task.Exception!.Flatten().InnerExceptions
If we add the "!" after the "Exception" property, the warning is supressed.

I tend to think of this as the I-know-what-I'm-doing operator. Let's take a closer look at the code to better understand this.

Incorrect Warning

For the most part, the warnings that we get when nullability is enabled are pretty accurate. But sometimes, there is not enough information, and a warning is incorrect. We'll use the same sample code from the prior articles: https://github.com/jeremybytes/nullability-in-csharp.

As a reminder, the "StartingCode" folder has the starting state of the code at the beginning of the first article in the series. The "FinishedCode" has the completed code. The links in this article will point to the "FinishedCode" folder unless otherwise noted.

Here's an example from inside the "FetchWithTaskButton_Click" method in the "MainWindow.xaml.cs" file of the "UsingTask.UI" project: 


    if (task.IsFaulted)
    {
        foreach (var ex in task.Exception.Flatten().InnerExceptions)
            Log(ex);
    }

The "Exception" property on the "task" variable is nullable. So we get a warning:


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

This is the same warning that we have seen in previous articles. But in this case, the warning is incorrect.

Task.IsFaulted
The reason this is incorrect is because of the "if" statement that checks the "IsFaulted" property on "task".

When a task is faulted, it means that an exception was thrown during the task execution. If the "IsFaulted" property is true, then the "Exception" property will be populated (meaning, it is not null).

So even though the "Exception" property is nullable, we know that it will not be null in this block of code.

One way of fixing this is to add a "#pragma" to the code to suppress the warning. Personally, I really don't like this approach because it clutters up the code. (If you are curious, you can use "Ctrl+." to have Visual Studio add the "#pragma" for you.)

Fortunately, there is another solution: use the null forgiving operator.

Null Forgiving Operator - !

With the null conditional operator (from the last article), we add a question mark after the object that may be null. The null forgiving operator uses an exclamation point.

Here is the updated code:


    if (task.IsFaulted)
    {
        foreach (var ex in task.Exception!.Flatten().InnerExceptions)
            Log(ex);
    }

With the operator added, the warning is suppressed. This is a way of saying "I know what I'm doing. This object is not going to be null here".

Another Example

As another example, let's look at the return value of a method, specifically the "GetAsync" method in the "PersonReader.cs" file of the "UsingTask.UI" project. (Note: this snippet is from the "StartingCode" folder.)


    public async Task<List<Person>> GetAsync(
        CancellationToken cancelToken = new CancellationToken())
    {
        [collapsed code]

        var result = JsonSerializer.Deserialize<List<Person>>(stringResult, options);
        return result;
    }
    'result' may be null here.
    CS8603: Possible null reference return.

The "GetAsync" method returns a "Task<List<Person>>". The "List<Person>" part is non-nullable.

The "Deserialize" method on the "JsonDeserializer" may return null (depending on the string that is being parsed).

So in the code sample, the "result" variable may be null. Because of this, we get a warning that we have a "possible null reference return".

In this case, I'm an not particularly concerned about the "Deserialize" method returning null. This is a known service (that I control), and the method has checks in place to make sure the service call is successful.

Ignoring the Warning
We can use the null forgiving operator to ignore the warning by putting the exclamation mark after the "result".


    public async Task<List<Person>> GetAsync(
        CancellationToken cancelToken = new CancellationToken())
    {
        [collapsed code]

        var result = JsonSerializer.Deserialize<List<Person>>(stringResult, options);
        return result!;
    }

This suppresses the null warning. But this may not be the best option for this code.

Null Coalescing Operator - ??
Another option is to use the null coalescing operator:


    public async Task<List<Person>> GetAsync(
        CancellationToken cancelToken = new CancellationToken())
    {
        [collapsed code]

        var result = JsonSerializer.Deserialize<List<Person>>(stringResult, options);
        return result ?? new List<Person>();
    }

Instead of suppressing the warning, the null coalescing operator says "if 'result' is null, then return a new empty list."

We will take a look at the null coalescing operator in the next article.

Wrap Up

The null analyzers are not always accurate. Sometimes we get null warnings on objects that we know will not be null. In these cases, we can use the null forgiving operator to suppress the warnings.

Enabling nullability gives us warnings to help us eliminate possible nulls in our code. And the null operators (including the null forgiving operator) can help us in dealing with those warnings.

In the next article, we will look at the null coalescing operators. These give us additional tools when dealing with possible nulls in our code. Be sure to check back details.


Happy Coding!

Monday, July 18, 2022

Null Conditional Operators in C# - ?. and ?[]

In the last article, we took at look at Nullability in C# - What It Is and What It Is Not. When we have nullability enabled, we need to respond to the warnings that come up in our code. This often means performing null checks or giving the compiler hints to what our code is doing. Fortunately, we have a number of null operators in C# to help us with those tasks. To continue with the series of articles exploring nullability, we will look at the null conditional operators (represented by question mark dot '?.' and question mark square brackets '?[]').

Articles

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

The Short Version

The null conditional operators give us a shortened syntax for checking for a null object before calling a method, reading a property, indexing into an object, or accessing another member on a nullable object.

Note: We will focus on the ?. operator for the samples since this is the more common of the 2 operators.

Let's look at 2 blocks of code that are almost equivalent.

This first version checks to make sure that the "tokenSource" field is not null before calling the "Cancel" method. If the field is null, then "Cancel" is not called:


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

This second version also checks to make sure that the "tokenSource" field is not null before calling the "Cancel" method. If the field is null, then "Cancel" is not called:


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

I say "almost equivalent" because the null conditional operator in the second version gives us a little bit more -- it also helps with thread safety.

Let's look at these things in more detail.

"Possibly Null" Warning

We are looking at the same code as the previous article. This can be found in the "MainWindow.xaml.cs" file in the "UsingTask.UI" project of the GitHub repository.

As a reminder, the "StartingCode" folder has the starting state of the code at the beginning of the first article in the series. The "FinishedCode" has the completed code. The links in this article will point to the "FinishedCode" folder.

Our code has a nullable "tokenSource" field defined in the class (from the "MainWindow.xaml.cs" file noted above):


    CancellationTokenSource? tokenSource;

    public MainWindow()
    {
        InitializeComponent();
    }

Since we have a "?" at the end of the field type "CancellationTokenSource", we are indicating that this field can be null. (And since we are not assigning a value to it in the constructor, it will be null initially).

This field is used in the Cancel button's event handler:

As we saw in the last article, in the code's initial state, we get a warning when we access the "tokenSource" field:


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

The warning tells us that the field may be null and result in a null reference exception.


CS8602: Dereference of a possibly null reference.

This gives us a warning that "tokenSource" may be null here. And if we call the "Cancel" method on a null field, we will end up with a null reference exception at runtime. (If you'd like to see this, it is shown in the previous article.)

Checking for Nulls

The traditional way to check for nulls is to wrap the code in a guard clause. This is often done with an "if" conditional:


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

The guard clause makes sure that the "tokenSource" field is not null before it attempts to call the "Cancel" method.

***THREAD SAFETY WARNING***
When we have code that references class-level fields (or other variables external to a method), there is a possibility of running into problems with threading. This is especially important to keep in mind since so much of the code we write today is asynchronous.

So what can happen here?

There is a brief period of time between when we check for the null in the "if" condition and the when we actually run the "Cancel" method. In this brief period it is possible that another method has set the "tokenSource" property to null.

The result of this would be a null reference exception when the code tries to call the "Cancel" method.

To get around this, developers will often make a local copy of the field or variable and act on that, particularly when dealing with delegates. No one else can affect the state of the local variable inside the method, so the "Cancel" method could be called with confidence.

However, there is an easier way to deal with this thread safety issue: use the null conditional operator.

Null Conditional Operator - ?.

The most common null conditional operator consists of a question mark and a dot. 

Note: The other null conditional operator uses a question mark with square brackets - ?[]. This is used to access indexers, and we will look at this briefly below. For more information, take a look at the Microsoft docs site on null conditional operators: Member Access Operators.

Here is what this null conditional operator looks like in use:


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

This has the same overall effect of the guard clause. At runtime, the "tokenSource" field is checked for null. If the field is not null, then the "Cancel" method is called normally.

If the field is null, the operation stops. The "Cancel" method is not called.

But a shortened syntax is not all that we get with the null conditional operator. We also get thread safety.

Thread Safety
The language designers took threading into account when they created this operator. From the developer's perspective, the null check and "Cancel" method call happen as a single operation, meaning that there is no possibility that something else would be able to set the "tokenSource" field to null in the middle.

I say "from the developer perspective" because the implementation is a little more complex than that. If you're curious, you can always fire up ILDASM (which is still included with the Visual Studio developer tools) and look at the IL (intermediate language) that is generated by the compiler.

What About Return Values or Properties?

The next question is what happens if we use the null conditional operator to call a method that returns a value or reads a property?

The short answer is that if the null conditional operator encounters a null, then the return value or property will be returned as "null".

As an example, let's say that we only want to call the "Cancel" method if the cancellation token is not already in a "canceled" state. Our "tokenSource" field has a property called "IsCancellationRequested" that we can use to check that.

Here's what that code may look like (note: this is not in the final code sample):


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

This uses an "if" statement to check the "IsCancellationRequested" property on the "tokenSource" field.

But we have some red squigglies. Here is the error message:


CS0266: Cannot implicitly convert type 'bool?' to 'bool'. An explicit conversion exists (are you missing a cast?)

This tells us that "if" needs a non-nullable Boolean value ("bool"). But since we use the null conditional operator, when we try to read "IsCancellationRequested", we may get a "null" back (this is noted with the nullable Boolean ("bool?") in the message).

We won't go through the trouble of fixing this code here. This sample is to show what happens when we use the null conditional operator on something that returns a value (either by calling a method or reading a property). In those cases, we may get a "null" returned.

Null Coalescing Operator - ??
In a future article, we will take a look at the null coalescing operator that lets us return a default value if we run across a null. This can help us fix things like the scenario above and eliminate possible null values.

Null Conditional Operator - ?[]

The null conditional operator that consists of a question mark and a set of square brackets is used for indexers on objects. The behavior is very similar to using the ?. operator to access a property.

For example, let's consider the following code (note: this is not part of the sample code on GitHub):


    List<Person>? people = null;
    Person? firstPerson = people?[0];

In this code, "people" is a nullable list of "Person" objects (and we set it to null). When we try to index into the "people" list, we put a question mark between "people" and the square brackets with the indexer.

If "people" is null, the indexer is not accessed, and so we do not get a null reference exception here. The "firstPerson" variable would then be assigned "null".

If "people" is not null, the indexer is accessed, and the first item in the collection will be returned.

As a side note, if the list is empty, we will get an "Index Out of Range" exception at runtime. But this is something we normally have to deal with regardless of whether nullability is enabled.

So we can use the ?[] null conditional operator to index into a nullable object. And this works similarly to using the ?. null conditional operator to access a property on a nullable object. If the object is null, then we get a null back. If the object is not null, then we get the item at the index or the value of the property.

Wrap Up

The null conditional operators can help us in 2 ways. First they can shorten our code by eliminating the need for a separate guard clause that specifically checks for "null". Secondly, they give us thread safety during the null check, so we do not need to worry about the possibility of a "null" sneaking into our code in a multi-threaded or async scenario.

Enabling nullability gives us the warnings we need to eliminate possible nulls in our code. And the null operators (including the null conditional operators) can help us in dealing with those warnings.

In the next 2 articles, we will look at the null forgiving operator as well as the null coalescing operators. These give us additional tools when dealing with possible nulls in our code. Be sure to check back for more.


Happy Coding!

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!