Programming with null

As described in Chapter 3, while null can be a useful value, it also comes with a few challenges—namely, the need to check a value isn’t null before invoking the object’s member or changing the value from null to something more appropriate to the circumstance.

Although you can check for null using the equality operators and even the relational equality operators, there are several other ways to do so, including C# 7.0’s support for is null and C# 10.0’s support for is not null. In addition, several operators are designed exclusively for working with the potential of a null value. These include the null-coalescing operator (and C# 8.0’s null-coalescing assignment) and the null-conditional operator. There is even an operator to tell the compiler when you believe a value isn’t null even if it isn’t obvious to the compiler—the null-forgiving operator. Let’s start by simply checking whether a value is null or not.

Checking for null and Not null

It turns out there are multiple ways to check for null, as shown in Table 4.4. For each listing, assume a declaration of string? uriString precedes it:

string? uriString = null;

Table 4.4: Checking for null

Description

Example

is null Operator Pattern Matching

The is operator provides multiple approaches to check for null. Starting with C# 7.0, you can check whether a value is null using the is null expression. This is a succinct and clear approach to checking if a value is null.

// 1.

if (uriString is null)

{

     Console.WriteLine(

         "Uri is null");

}

is not null Operator Pattern Matching

Similarly, in C# 9.0, is not null support was added. And, when checking for not null, this is the preferred approach.

// 2.

if (uriString is not null)

{

     Console.WriteLine(

         "Uri is not null");

}

Equality/Inequality

Using the equality and inequality operators works with all versions of C#.

In addition, checking for null this way is readable.

The only disadvantage of this approach is that it is possible to override the equality/inequality operator, potentially introducing minor performance impacts.

// 3.

if(uriString == null)

{

     Console.WriteLine(

         "Uri is null");

}

if(uriString != null)

{

     Console.WriteLine(

         $"Uri is: { uriString }");

}

is object

Since C# 1.0, is object checks whether the operand is null although this syntax isn’t as clear as is not null.

is object is preferable over the is {} expression described in the next row because it issues a warning when the operand is a non-nullable value type. What is the point in checking for null if the operand can’t be null?

// 4.

int number = 0;

if( (uriString is object )

     // Warning CS0183: The given

     // expression is always not

     // null.

     && (number is object)

)

{

     Console.WriteLine(

         $"Uri is: { uriString }");

}

is { } Operator Pattern Matching

The property pattern matching expression, <operand> is { }, (which was added in C# 8.0) provides virtually the same functionality as is object. Besides being less readable, the only noticeable disadvantage is that is {} with a value type expression doesn’t issue a warning, while is object does. And, since a value type cannot be null, the warning is preferable since there is no point in checking a non-nullable value for null. Therefore, favor using is object over is { },

// 5.

if (uriString is {})

{

     Console.WriteLine(

         $"Uri is: { uriString }");

}

ReferenceEquals()

While object.ReferenceEquals() is a somewhat long syntax for such a simple operation, like equality/inequality it works with all versions of C# and has the advantage of not allowing overriding; thus, it always executes what the name states.

// 6.

if(ReferenceEquals(

   uriString, null))

{

     Console.WriteLine(

         "Uri is null");

}

Of course, having multiple ways to check whether a value is null raises the question as to which one to use. C# 7.0’s enhanced is null and C# 9.0’s is not null syntax are preferable if you are using a modern version of C#.

Obviously, if you are programming with C# 6.0 or earlier, the equality/inequality operators are the only option aside from using is object to check for not null. (The latter has a slight advantage since it is not possible to change the definition of the is object expression and (albeit unlikely) introduce a minor performance hit.) This renders is {} effectively obsolete. Using ReferenceEquals() is rare, but it does allow comparison of values that are of unknown data types, which is helpful when implementing a custom version of the equality operator (see “Operator Overloading” in Chapter 10).

Several of the rows in Table 4.4 leverage pattern matching, a concept covered in more detail in the “Pattern Matching with the is Operator” section of Chapter 7.

Null-Coalescing and Null-Coalescing Assignment Operators (??, ??=)

The null-coalescing operator is a concise way to express “If this value is null, then use this other value.” It has the following form:

expression1 ?? expression2

The null-coalescing operator also uses a form of short-circuiting. If expression1 is not null, its value is the result of the operation and the other expression is not evaluated. If expression1 does evaluate to null, the value of expression2 is the result of the operator. Unlike the conditional operator, the null-coalescing operator is a binary operator.

Listing 4.37 illustrates the use of the null-coalescing operator.

Listing 4.37: Null-Coalescing Operator
string? fullName = GetSaveFilePath();
// ...
 
// Null-coalescing operator
string fileName = GetFileName() ?? "config.json";
string directory = GetConfigurationDirectory() ??
    GetApplicationDirectory() ??
    Environment.CurrentDirectory;
 
// Null-coalescing assignment operator
fullName ??= $"{ directory }/{ fileName }";
 
// ...

In this listing, we use the null-coalescing operator to set fileName to "config.json" if GetFileName() is null. If GetFileName() is not null, fileName is simply assigned the value of GetFileName().

The null-coalescing operator “chains” nicely. For example, an expression of the form x ?? y ?? z results in x if x is not null; otherwise, it results in y if y is not null; otherwise, it results in z. That is, it goes from left to right and picks out the first non-null expression, or uses the last expression if all the previous expressions were null. The assignment of directory in Listing 4.37 provides an example.

C# 8.0 provides a combination of the null-coalescing operator and the assignment operator with the addition of the null-coalescing assignment operator. With this operator, you can evaluate if the left-hand side is null and assign the value on the righthand side if it is. Listing 4.37 uses this operator when assigning fullName.

Null-Conditional Operator (?., ?[])

In recognition of the frequency of the pattern of checking for null before invoking a member, you can use the ?. operator, known as the null-conditional operator,4 as shown in Listing 4.38 5.

Listing 4.38: Null-Conditional Operator
string[]? segments = null;
string? uri = null;
 
// ...
 
int? length = segments?.Length;
// ...
if (length is not null && length != 0)
{
    uri = string.Join('/', segments!);
}
 
if (uri is null || length is 0)
{
    Console.WriteLine(
        "There were no segments to combine.");
}
else
{
    Console.WriteLine(
        $"Uri: { uri }");
}

The null-conditional operator example (int? length = segments?.Length) checks whether the operand (the segments in Listing 4.38) is null prior to invoking the method or property (in this case, Length). The logically equivalent explicit code would be the following (although in the original syntax, the value of segments is evaluated only once):

int? length =

     (segments != null) ? (int?)segments.Length : null

An important thing to note about the null-conditional operator is that it always produces a nullable value. In this example, even though the string.Length member produces a non-nullable int, invoking Length with the null-conditional operator produces a nullable int (int?).

You can also use the null-conditional operator with the array accessor. For example, uriString = segments?[0] will produce the first element of the segments array if the segments array was not null. Using the array accessor version of the null-conditional operator is relatively rare, however, as it is only useful when you don’t know whether the operand is null, but you do know the number of elements, or at least whether a particular element exists.

What makes the null-conditional operator especially convenient is that it can be chained (with and without more null-coalescing operators). For example, in the following code, both ToLower() and StartWith() will be invoked only if both segments and segments[0] are not null:

segments?[0]?.ToLower().StartsWith("file:");

In this example, of course, we assume that the elements in segments could potentially be null, so the declaration (assuming C# 8.0) would more accurately have been

string?[]? segments;

The segments array is nullable, in other words, and each of its elements is a nullable string.

When null-conditional expressions are chained, if the first operand is null, the expression evaluation is short-circuited, and no further invocation within the expression call chain will occur. You can also chain a null-coalescing operator at the end of the expression so that if the operand is null, you can specify which default value to use:

string uriString = segments?[0]?.ToLower().StartsWith(

     "file:")??"intellitect.com";

Notice that the data type resulting from the null-coalescing operator is not nullable (assuming the right-hand side of the operator ["intellitect.com" in this example] is not null—which would make little sense).

Be careful, however, that you don’t unintentionally neglect additional null values. Consider, for example, what would happen if ToLower() (hypothetically, in this case) returned null. In this scenario, a NullReferenceException would occur upon invocation of StartsWith(). This doesn’t mean you must use a chain of null-conditional operators, but rather that you should be intentional about the logic. In this example, because ToLower() can never be null, no additional null-conditional operator is necessary.

Although perhaps a little peculiar (in comparison to other operator behavior), the return of a nullable value type is produced only at the end of the call chain. Consequently, calling the dot (.) operator on Length allows invocation of only int (not int?) members. However, encapsulating segments?.Length in parentheses—thereby forcing the int? result via parentheses operator precedence—will invoke the int? return and make the Nullable<T> specific members (HasValue and Value) available. In other words, segments?.Length.Value won’t compile because int (the data type returned by Length) doesn’t have a member called Value. However, changing the order of precedence using (segments?.Length).Value will resolve the compiler error because the return of (segments?.Length) is int?, so the Value property is available.

Null-Forgiving Operator (!)

Notice that the Join() invocation of Listing 4.38 includes an exclamation point after segments:

uriString = string.Join('/', segments!);

At this point in the code, segments?.Length is assigned to the length variable, and since the if statement verifies that length is not null, we know that segments can’t be null either.

int? length = segments?.Length;

if (length is not null && length != 0){ }

However, the compiler doesn’t have the capability to make the same determination. And, since Join() requires a non-nullable string array, it issues a warning when passing an unmodified segments variable whose declaration was nullable. To avoid the warning, we can add the null-forgiving operator (!), starting in C# 8.0. It declares to the compiler that we, as the programmer, know better, and that the segments variable is not null. Then, at compile time, the compiler assumes we know better and dismisses the warning (although the runtime still checks that our assertion is not null at execution time).

Note that while the null-conditional operator checks that segments isn’t null, it doesn’t check how many elements there are or check that each element isn’t null.

In Chapter 1 we encountered warning CS8600, “Converting null literal or possible null value to non-nullable type,” when assigning Console.ReadLine() to a string. The occurs because Console.ReadLine() returns a string? rather than a non-nullable string. In reality, Console.ReadLine()returns null only if the input is redirected, which isn’t a use case we are expecting in these intro programs, but the compiler doesn’t know that. To avoid the warning, we could use the null-forgiving operator on the Console.ReadLine() return—stating we know better that the value returned won’t be null (and if it is, we are comfortable throwing a null-reference exception).

string text = Console.ReadLine()!;

AdVanced Topic
Leveraging the Null-Conditional Operator with Delegates

The null-conditional operator is a great feature on its own. However, using it in combination with a delegate invocation resolves a C# pain point that has existed since C# 1.0. Notice in Listing 4.39 how the PropertyChanged event handler is assigned to a local copy (propertyChanged) before we check the value for null and finally fire the event. This is the easiest thread-safe way to invoke events without running the risk that an event unsubscribe will occur between the time when the check for null occurs and the time when the event is fired. Unfortunately, this approach is nonintuitive, and frequently developers neglect to follow this pattern—with the result of throwing inconsistent NullReferenceExceptions. Fortunately, with the introduction of the null-conditional operator, this issue has been resolved.

Listing 4.39: Null-Conditional Operator with Event Handlers
System.EventHandler propertyChanged =
    PropertyChanged;
if (propertyChanged != null)
{
    propertyChanged(this,
        new System.EventArgs());
}

The check for a delegate value changes from what is shown in Listing 4.39 to simply

PropertyChanged?.Invoke(propertyChanged(

   this, new PropertyChangedEventArgs(nameof(Name)));

Because an event is just a delegate, the same pattern of invoking a delegate via the null-conditional operator and an Invoke() is always possible.

________________________________________

4. Introduced in C# 6.0.
5. This code could be improved with a using statement—a construct that we have avoided because it has not yet been introduced.
{{ snackbarMessage }}