My Favorite C# Features - Part 3: Nullability

Jeffrey T. Fritz - Apr 9 '21 - - Dev Community

I'm a practical programmer. I don't like to over-optimize my code, and I want to make it very readable for the next person who needs to work with something that I wrote. Consequently, I sometimes leave alternate interpretations and access patterns into my code that might not always work as expected. A great example of this is the ability to pass null into C# methods and trigger a different behavior. This can lead to errors in future code where you are now accessing something that inadvertently is a null object.

In C# 8, the langauge designers introduced a feature called Nullable Reference Types that allows you to define which variables could be null and which variables should NEVER be null.

In this article, and as an effort to help make myself a better programmer, we're going to review the nullable Reference Types feature of C# and discuss why it is an important feature that we should start using by default in our applications.

Why null?

As an object oriented language, C# has always had the concept of null in code. null is the absence of an object, its synonymous with "nothing" and is an easy concept for folks to understand when you declare a variable. However, this can (and most likely WILL) lead to the dreaded NullReferenceException, an error that indicates a null object was acted on unexpectedly.

PRO TIP: Sometimes, you'll hear C# programming folks refer to a NullReferenceException as an NRE.

Logger myConsoleLogger = default(ILogger);
myConsoleLogger.LogInformation("Processed the data");
Enter fullscreen mode Exit fullscreen mode

In the sample above, the myConsoleLogger object is declared and assigned its default value (which is null). This would trigger a NullReferenceException because myConsoleLogger was never assigned an instance of an object. This is a simple mistake, but it would be really nice if the compiler caught this before we even tried to run the code. Consider a scenario like this array:


string[] values = new string[3];
string firstValue = values[0];
Console.WriteLine(firstValue.ToLower());

Enter fullscreen mode Exit fullscreen mode

This is going to throw a NullReferenceException also, because the values array is declared but never assigned values. The firstValue variable is initialized with a null value on line 2 and the ToLower() method is then attempting to operate on a null object. Once again, a simple error because the values array is never assigned values.

Nullable Contexts and Compiler Warnings to the Rescue!

Some folks fear the compiler. There's a feeling that the compiler throwing errors or emitting warnings is an intimidating practice. I see it the other way: The compiler is my friend telling me when I made a mistake before I attempt to run my application. In this case, I want the compiler to tell me when I might work with a null object because the variables weren't initialized properly. Let's get some help to ensure that our objects in our code are being used correctly.

By default in C#, any reference type can be assigned the null value. With C# 8 and .NET Core 3.0 and later, we can define contexts in and around our projects where the compiler will perform nullability checks and raise warnings if we are potentially going to throw a NullReferenceException.

We can enable nullable checking on a segment of code by wrapping it with a compiler pre-processor #nullable with a setting the indicates how it should behave. Let's add nullability checking to the Hat class I introduced in the previous post in this series:

#nullable enable

    public class Hat {

        public string Name { get; set; }

        public int AcquiredYear { get; set; }

        public string Theme { get; set; }

    }

#nullable restore
Enter fullscreen mode Exit fullscreen mode

There are two pre-processor commands present in this code: #nullable enable and #nullable restore. The enable command tells the compiler to check for variables that could be inadvertently assigned null and raise a compiler warning if there are any. Sure enough, in the Hat class, the string properties Name and Theme need to be initialized according to these compiler warnings:

warning CS8618: Non-nullable property 'Name' must contain a non-null value when exiting 
constructor. Consider declaring the property as nullable. 
warning CS8618: Non-nullable property 'Theme' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Enter fullscreen mode Exit fullscreen mode

This are easy fixes as I can default the values for these two properties to string.Empty:


#nullable enable
public class Hat {

  public string Name { get; set; } = string.Empty;

  public int AcquiredYear { get; set; }

  public string Theme { get; set; } = string.Empty;

}
#nullable restore

Enter fullscreen mode Exit fullscreen mode

The compiler warnings go away, and I am a happy developer. If I try to define a Hat and assign null to these fields, will the compiler catch it?

var newHat = new Hat { 
  Name="Phillies 80's Maroon", 
  AcquiredYear=1985, 
  Theme=null 
};
Enter fullscreen mode Exit fullscreen mode

This code DOES compile with no warnings. Why? The #nullable compiler directive is set on the Hat class, not the construction of the newHat variable. In order to protect more of our code, we need to expand the nullable check's scope to include more of our code.


#nullable enable

var newHat = new Hat { 
  Name="Phillies 80's Maroon", 
  AcquiredYear=1985, 
  Theme=null 
};

#nullable restore

Enter fullscreen mode Exit fullscreen mode

This raises the appropriate compiler warning:

warning CS8625: Cannot convert null literal to non-nullable reference type.
Enter fullscreen mode Exit fullscreen mode

Let's review quickly: these are ONLY compiler warnings. In fact, this code will run and not produce any errors.

Declaring a Reference Type as Nullable

I can also tell the compiler that it's ok if one of these reference values is assigned null by attaching a ? to the end of it's variable declaration:


public class Hat {

  public string? Name { get; set; }

  public int AcquiredYear { get; set; }

  public string Theme { get; set; } = string.Empty;
}

Enter fullscreen mode Exit fullscreen mode

With this change, the Name of the hat is allowed to be assigned null regardless of the Nullable context around the class.

Project-wide Nullable Checking

What if I want to roll-out this compiler interaction across my ENTIRE project. You can add an entry to your project file that indicates nullable checking should be enabled with the Nullable element:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
Enter fullscreen mode Exit fullscreen mode

We can now remove the #nullable directives from the Hat class and I'll receive the same compiler warnings without writing more code.

Disable Nullable Checking

What if I have an application that is configured with the Nullable entry in the project file, and I want to relax the checking on various sections of my application?

Similar to before, we can add a processor directive to our code that disables null checking:


#nullable disable

var newHat = new Hat { 
  Name="Phillies 80's Maroon", 
  AcquiredYear=1985, 
  Theme=null 
};

#nullable restore

Enter fullscreen mode Exit fullscreen mode

Now we're talking... I can enforce the better developer behavior by adding the Nullable check to my project file, and those developers that want to take the risk of assigning and working with null can wrap their code with the processor to remove the warnings.

The Final Step

Warnings are just silly yellow text the compiler emits that tells us we MIGHT have a concern in our project. Did you know that you can turn up the importance of this warnings, converting them to errors the compiler emits and ensuring that you write better code? Add the TreatWarningsAsErrors element to your project file and those pesky warnings become a real problem that blocks your project from compiling properly:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>

</Project>
Enter fullscreen mode Exit fullscreen mode

Now our project team will be forced to treat null values with more respect.

Summary

The ability to assign and work with the null value is valuable in C#, but can be misused and lead to errors in our running applications. Let's get some help from the compiler to make handling of null values easier and clearer when we're building our projects.

Did you know, I host a weekly live stream on the Visual Studio Twitch channel teaching the basics of C#? Tune in on Mondays at 9a ET / 1300 UTC for two hours of learning in a beginner-friendly Q+A format with demos and sample code you can download.

Looking to get started learning C#? Checkout our free on-demand courses on Microsoft Learn!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .