Declaring Types That Allow null

Often it is desirable to represent values that are “missing.” When specifying a count, for example, what do you store if the count is unknown or unassigned? One possible solution is to designate a “magic” value, such as -1 or int.MaxValue. However, these are valid integers, so it can be ambiguous as to when the magic value is a normal int or when it implies a missing value. A preferable approach is to assign null to indicate that the value is invalid or that the value has not been assigned. Assigning null is especially useful in database programming. Frequently, columns in database tables allow null values. Retrieving such columns and assigning them to corresponding variables within C# code is problematic, unless the data type in C# can contain null as well.

You can declare a type as either nullable or not nullable, meaning you can declare a type to allow a null value or not, with the nullable modifier.1 To enable nullability, simply follow the type declaration with a nullable modifier—a question mark immediately following the type name. For example, int? number = null will declare a variable of type int that is nullable and assign it the value null. Unfortunately, nullability includes some pitfalls, requiring the use of special handling when nullability is enabled.

Dereferencing a null Reference

While support for assigning null to a variable is invaluable (pun intended), it is not without its drawbacks. While copying or passing a null value to other variables and methods is inconsequential, dereferencing (invoking a member on) an instance of null will throw a System.NullReferenceException—for example, invoking text.GetType() when text has the value null. Anytime production code throws a System.NullReferenceException, it is always a bug. This exception indicates that the developer who wrote the code did not remember to check for null before the invocation. Further exacerbating the problem, checking for null requires an awareness on the developer’s part that a null value is possible and, therefore, an explicit action is necessary. It is for this reason that declaring of a nullable variable requires explicit use of the nullable modifier—rather than the opposite approach where null is allowed by default (see “Nullable Reference Types” later in the section). In other words, when the programmer opts to allow a variable to be null, they takes on the additional responsibility of being sure to avoid dereferencing a variable whose value is null.

Since checking for null requires the use of statements and/or operators that we haven’t discussed yet, the details on how to check for null appear in “Advanced Topic: Checking for null.” Full explanations, however, are in Chapter 4.

AdVanced Topic
Checking for null

There are numerous statements and operators that developers can use to check for null. Listing 3.1 provides a few examples. The clearest way to check for null is with an if statement and the is operator, as demonstrated in Listing 3.1.

Listing 3.1: Checking for null
public static void Main(string[] args)
{
    int? number = null;
    // ...
    if (number is null)
    {
        Console.WriteLine(
            "'number' requires a value and cannot be null");
    }
    else
    {
        Console.WriteLine(
            $"'number' doubled is { number * 2 }.");
    }
}

The if statement checks whether number is null and then takes different actions depending on the condition. Although you can also use the equality operator (==), it is possible to change the way the equality operator behaves (with overriding); thus, using the is keyword is preferable.

Another useful operator2 when working with null is the null-conditional operator. It first checks for null before dereferencing a nullable value. For example, int? length = text?.Length; automatically returns null if text has the value null and the length of the string stored in text otherwise. Notice that because the value returned from text?.Length could be null (if text is null), the variable length must be declared as nullable.

Both the if statement and the null-conditional operator are described in more detail in Chapter 4. While the is keyword also appears in Chapter 4 briefly, the full explanation isn’t provided until Chapter 7, which discusses the concept of pattern matching.

Nullable Value Types

Since a value type refers directly to the actual value, value types cannot innately contain a null because, by definition, they cannot contain references, including references to nothing. Nonetheless, we still use the term “dereferencing a value type” when invoking members on the value type. In other words, while not technically correct, using the term “dereferencing” when invoking a member, regardless of whether it is a value type, is common.3

AdVanced Topic
Dereferencing null on Value Types

Technically, value types declared with the nullable modifier are still value types, so even though they have null behavior, under the covers they are not actually null. Thus, for the most part, dereferencing a nullable value type that represents null will not throw a nullable exception. Members like HasValue, ToString(), and even the equality-related members (GetHashCode() and Equals()) are all implemented on Nullable<T>, so they won’t throw exceptions when the value is representing null. (Instead, dereferencing a null value type throws an InvalidOperationException—not a NullReferenceException—to remind programmers that they should check for a value before dereferencing.) However, invoking GetType() when the value represents null does throw a NullReferenceException. The inconsistency occurs because GetType() is not a virtual method, so Nullable<T> can’t overload the behavior. That leaves the default—throwing a NullReferenceException.

Nullable Reference Types

Prior to C# 8.0, all reference type variables allowed null. Unfortunately, this resulted in numerous bugs because avoiding a null reference exception required the developer to realize the need to check for null and defensively program to avoid dereferencing the null value. Further exacerbating the problem, reference type variables are nullable by default. If no value is assigned to a variable of reference type, the value will default to null. Moreover, if you dereferenced a reference-type local variable whose value was unassigned, the compiler would (appropriately) issue an error, "Use of unassigned local variable 'text'", for which the easiest correction was to simply assign null during declaration, rather than to ensure a more appropriate value was assigned regardless of the path that execution might follow (see Listing 3.2). In other words, developers would easily fall into the trap of declaring a variable and assigning a null value as the simplest resolution to the error, (perhaps mistakenly) expecting the code would reassign the variable before it was dereferenced.

Listing 3.2: Dereferencing an Unassigned Variable
#nullable enable
public static void Main()
{
    string? text;
    // ...
    // Compile Error: Use of unassigned local variable 'text'
    System.Console.WriteLine(text.Length);
}

In summary, the nullability of reference types by default was a frequent source of defects in the form of System.NullReferenceExceptions, and the behavior of the complier led developers astray unless they took explicit actions to avoid the pitfall.

To improve this scenario significantly, the C# team introduced the concept of nullability to reference types in C# 8.0—a feature known as nullable reference types (implying, of course, that reference types could be non-nullable as well). Nullable reference types bring reference types on par with value types, in that reference type declarations can occur with or without a nullable modifier. In C# 8.0, declaring a variable without the nullable modifier implies it is not nullable.

Unfortunately, supporting the declaration of a reference type with a nullable modifier and defaulting the reference type declaration with no null modifier to non-nullable has major implications for code that is upgraded from earlier versions of C#. Given that C# 7.0 and earlier supported the assignment of null to all reference type declarations (i.e., string text = null), does all the code fail compilation in C# 8.0?

Fortunately, backward compatibility is extremely important to the C# team, so support for reference type nullability is not enabled by default on existing projects. In contrast, on new projects like the HelloWorld program in Chapter 1, nullability is enabled at the start.

There are several mechanisms to configure nullability: the #nullable directive, project properties, and even the command line. First, the null reference type feature is activated in this example with the #nullable directive:

#nullable enable

The directive supports values of enable, disable, and restore—the last of which restores the nullable context to the project-wide setting. Listing 3.2 provides an example that sets nullable to enable with a nullable directive. In so doing, the declaration of text as string? is enabled and no longer causes a compiler warning.

Alternatively, programmers can use project properties to enable reference type nullability. If not specified, a project file’s (*.csproj) project-wide setting has nullable disabled. To enable it, include a Nullable project property whose value is enable (see Chapter 1, Listing 1.2 for the full listing).

<Nullable>enable</Nullable>

All sample code for this book has nullable enabled at the project level.

One last way to enable nullability is to set the project properties on the dotnet command line with the /p argument:

dotnet build /p:Nullable=enable

Specifying the value for Nullable on the command line will override any value set in the project file.

________________________________________

1. Technically, C# includes support for the nullable modifier only with value types in C# 2.0 and reference types in C# 8.0.
2. Added in C# 6.0.
3. Nullable value types were introduced with C# 2.0.
{{ snackbarMessage }}
;