Null Values in C# – Part 2
Using nullable reference types in C# 8.0 to control variable nullability and prevent null exceptions.
Unaccounted for null values in C# code can cause unexpected null reference and argument null exceptions, leading to poor application performance and program termination. Part one of Null Values in C# looked at using operators to prepare code for a null occurrence. This required the developer to always be aware of when a variable could be null and keep track of these variables in complicated programs. C# 8.0 introduces nullable reference types, which allows developers to specify upfront whether a reference variable can be assigned null or not. And the compiler can use static flow analysis to find potential unhandled uses of null and generate warnings.
First is a quick guide to types in C#. Then some information on nullable contexts and how enabling or disabling the contexts will affect reference types. Followed by some code examples and how nullable reference types can be used to prevent null exceptions.
The C# type system has two base types: value types and reference types. Built-in types, such as bool, char, int, decimal, and other numeric types, are value types. Structs and enums are also value types. Value types are sealed and can’t be derived from. However, structs can implement interfaces and be cast to those interface objects, which uses a boxing operation. Value types cannot be null. However, C# does allow for these types to be null (with Nullable<T>), called nullable value types. Appending a ‘?’ to the value type at declaration allows that variable to be null, such as int? or bool?
Reference types are types defined using class, interface, or delegate and when any type is declared as an array. Inheritance is supported by reference types. Variables of a reference type are null when declared and can be assigned objects that are explicitly created using the new keyword (to create the objects, space is allocated on the managed heap) or assigned to an existing object of that type. Reference type variables hold the location of the object and can be assigned null anywhere in the code.
With C# 8.0, reference types have nullability which is one of four options: non-nullable (can’t be assigned null), nullable (can be assigned null), oblivious (it is unknown if the type can be null or not, this is pre C# 8.0 behavior), and unknown (constraints don’t tell the compiler if the type is nullable or nonnullable, like when using generics which could be passed value or reference types). Each type of nullability is summarized in the table below. A difference between the variable’s nullability and the usage will generate a warning.
Null and reference types
With the introduction of nullable and non-nullable reference types the developer can make decisions on which types can be assigned null (or have unassigned reference types). If the nullable context is enabled (more on this below) a non-nullable reference variable that is assigned null anywhere in the code will cause a warning. Nullable reference types are declared by appending a ‘?’ to the type (the same way a nullable value type is declared). Using the ? syntax for nullable reference and value types makes the code consistent and the developer’s intent for the variable to be null or non-null is clear.
Enable or disable nullable contexts
The nullable context gives developers the ability to enable/disable features for a project or for specific lines of code. This may be particularly useful for programs where existing code can’t be immediately updated but the developers wish to begin moving an application to full use of nullable reference types.
Two nullable context options are available, nullable annotation context and nullable warnings context. The nullable annotation context allows or removes the use of ? to declare a nullable reference type and !, the null forgiving operator. The nullable warnings context determines if the compiler will use static flow analysis to decide the null state of a variable and if nullability warnings should be generated. For a project, each of these is enabled or disabled based on the “Nullable” element in the .csproj, highlighted in the example below. The default value for Nullable is disable.
The nullable contexts can also be enabled or disabled for specific lines of code using directives. The directive begins with #nullable and is followed by enable or disable and optionally warnings or annotations. A few examples are shown below and there’s additional information in the documentation. Nullable enable or disable applies to both, the annotation context and the warnings context. Nullable restore will set both settings to the project settings.
Optionally the specific context the developers wishes to affect can also be specified with warnings or annotations.
A simple illustration of how each of these settings affects the code editor and compilers, consider the following reference variable declaration that is assigned null.
Assigning a null to a non-nullable reference type generates a warning
No warning for assigning null because the reference types are oblivious, however, nullability warnings are generated.
A reference type can be made nullable but nullability warnings are disabled.
Everything works the same as pre C# 8.0 (reference types are oblivious and no nullable warnings).
Using nullable reference types
Consider the following Tree class. This class has a property called Name.
And a function in a different class that takes a list of Tree objects.
Nullable Context: Disable
When the nullable context is set to disable, the following code is valid and does not generate any warnings. However, this code will throw Null Reference Exceptions (once for tree1.Name and again
inside the Water function).
Nullable Context: Annotations
Enabling the nullable annotation context without the warnings context will allow the reference types to be nullable, shown below. Nullable warnings are not generated based on the usage of the reference type.
So even though reference types are non-nullable, no warning is generated for assigning a reference type null.
Nullable Context: Warnings
When the nullable warnings context is enabled, the compiler finds the following issue, successfully detecting the use of a null reference.
If that line is changed to use a null conditional, ?., operator the warning is resolved. However, with just the warnings context, the compiler does not flag the second issue: using tree2 inside the Water function. This is because the nullable annotation context is not enabled so all reference variables, including tree2, are oblivious.
Nullable Context: Enable (Annotations and Warnings)
Next, the nullable annotations context and nullable warnings context is enabled for the same code. This results in the following potential issues detected.
All the reference types are non-nullable and the warnings are generated because each time a nonnullable type is assigned a null value. To fix this, tree1 could be initialized to a value other than null.
The null assignment is valid, and a warning is generated when tree1 is dereferenced without checking for null. The solution for this warning, like above is to use a null conditional operator when dereferencing tree1.
Now that tree1 is nullable, the compiler will detect and generate a waring when it is assigned to a nonnullable. Like when tree2 is assigned tree1.
And again, when tree2 is used in creating the list of trees.
These variables could be updated to all be nullable reference types. Notice where the code needed to bechanged in four places (highlighted below).
And there is still a warning in the Water function (when the items in the list are used) because the local tree variable is dereferenced without checking for null.
Null Forgiving Operator
If the developer knows for sure that none of the values will be null, then the null forgiving operator could be used. Null forgiving tells the compiler that even though the reference type could be null, the programmer is confident that it never will be, and a warning is not generated even though there isn’t a null check. Null forgiving operator is only used by the compiler’s static flow analysis and does not have any runtime effects.
Other Nullable Options
Alternatively, depending on the desired behavior of the code, this could be fixed with a null conditional and null coalescing operator where a null check is needed in the Water function.
Sometimes a change like this is not desired because it involves making a lot of reference variables nullable. An alternative option is to handle the null when tree2 is assigned tree1 (earlier in the code).
Now, only tree1 is a nullable reference type and all the other reference variables are non-nullable. The List does not contain nulls so the Name property can be used without the extra null checks. And the
code is not going to throw any null reference exceptions.
The features in C# 8.0 for nullable and non-nullable reference types will help developers write code that is very specific about null usage and catch unintentional consequences when variables are null. Nullable value types and nullable reference types are both declared with a ? appended to the type making the code consistent and improves readability. The nullable context can be customized for each application. The project level Nullable setting along with the ability to turn the nullable contexts on or off in code makes this an easy to use and valuable feature for existing and new applications.