Understanding values and references

Understanding null values and nullable types

When you declare a variable, it’s always a good idea to initialize it. With value types, it’s common to see code such as this:

int i = 0;
double d = 0.0;

To initialize a reference variable such as a class, you can create a new instance of the class and assign the reference variable to the new object, like this:

var c = new Circle(42);

This is all very well, but what if you don’t actually want to create a new object? Perhaps the purpose of the variable is simply to store a reference to an existing object at some later point in your program. In the following code example, the Circle variable copy is initialized, but later it is assigned a reference to another instance of the Circle class:

var c = new Circle(42); 
var copy = new Circle(99); // Some random value for initializing copy 
... 
copy = c;                     // copy and c refer to the same object

After assigning c to copy, what happens to the original Circle object with a radius of 99 that you used to initialize copy? Nothing refers to it anymore. In this situation, the runtime can reclaim the memory by performing an operation known as garbage collection, which you’ll learn more about in Chapter 14, “Using garbage collection and resource management.” The important thing to understand for now is that garbage collection is a potentially time-consuming operation, and that you should not create objects that are never used because doing so is a waste of time and resources.

You could argue that if a variable will be assigned a reference to another object at some point in a program, there’s no point to initializing it. This is poor programming practice, however, and can lead to problems in your code. For example, you will inevitably find yourself in a situation in which you want to refer a variable to an object only if that variable does not already contain a reference, as shown in the following code example:

var c = new Circle(42); 
Circle copy;                  // Uninitialized !!! 
... 
if (copy == // only assign to copy if it is uninitialized, but what goes here?) 
{ 
    copy = c; ;                // copy and c refer to the same object 
    ... 
}

The purpose of the if statement is to test the copy variable to see whether it is initialized, but to which value should you compare this variable? The answer is to use a special value called null.

In C#, you can assign the null value to any reference variable. The null value simply means that the variable does not refer to an object in memory. You can use it like this:

Circle c = new Circle(42);
Circle copy = null; // Initialized 
... 
if (copy is null) 
{ 
    copy = c; // copy and c refer to the same object 
    ... 
}

The null-conditional and null-coalescing operators

The null-conditional operator enables you to test for null values very succinctly. To use the null-conditional operator, you append a question mark (?) to the name of your variable.

For example, suppose you attempt to call the Area method on a Circle object when the Circle object has a null value:

Circle c = null; 
Console.WriteLine($"The area of circle c is {c.Area()}");

In this case, the Circle.Area method throws a NullReferenceException, which makes sense because you cannot calculate the area of a circle that does not exist.

To avoid this exception, you could test whether the Circle object is null before you attempt to call the Circle.Area method:

if (c is not null) 
{ 
    Console.WriteLine($"The area of circle c is {c.Area()}"); 
}

In this case, if c is null, nothing is written to the command window. Alternatively, you could use the null-conditional operator on the Circle object before you attempt to call the Circle.Area method:

Console.WriteLine($"The area of circle c is {c?.Area()}");

The null-conditional operator tells the C# runtime to ignore the current statement if the variable you have applied the operator to is null. In this case, the command window would display the following text:

The area of circle c is

Both approaches are valid and might meet your needs in different scenarios. The null-conditional operator can help you keep your code concise, particularly when you deal with complex properties with nested reference types that could all be null valued.

Alongside the null-conditional operator, C# provides two null-coalescing operators. The first of these, ??, is a binary operator that returns the value of the operand on the left if it isn’t null; otherwise, it returns the value of the operand on the right. In the following example, variable c2 is assigned a reference to c if c isn’t null; otherwise, it is assigned a reference to a new Circle object:

Circle c = ...; // might be null, might be a new Circle object
...
var c2 = c ?? new Circle(42) ;

The null-coalescing assignment operator, ??=, assigns the value of the operand on the right to the operand on the left only if the left operand is null. If the left operand references some other value, it is unchanged.

Circle c = ...; // might be null, might be a new Circle object
Circle c3 = ...; // might be null, might be a new Circle object
...
var c3 ??= c; // Only assign c3 if it is null, otherwise leave unchanged;

Using nullable types

The null value is very useful for initializing reference types. Sometimes, though, you need an equivalent value for value types. null is itself a reference, so you cannot assign it to a value type. The following statement is therefore illegal in C#:

int i = null; // illegal

However, C# defines a modifier that you can use to declare that a variable is a nullable value type. A nullable value type behaves similarly to the original value type, but you can assign the null value to it. You use the question mark (?) to indicate that a value type is nullable, like this:

int? i = null; // legal

You can ascertain whether a nullable variable contains null by testing it in the same way as you test a reference type.

if (i is null) 
      ...

You can assign an expression of the appropriate value type directly to a nullable variable. The following examples are all legal:

int? i = null;
int j = 99; 
i = 100; // Copy a value type constant to a nullable type 
i = j; // Copy a value type variable to a nullable type

You should note that the converse is not true. You cannot assign a nullable variable to an ordinary value type variable. So, given the definitions of variables i and j from the preceding example, the following statement is not allowed:

j = i; // illegal

This makes sense when you consider that the variable i might contain null, and j is a value type that cannot contain null. This also means that you cannot use a nullable variable as a parameter to a method that expects an ordinary value type. If you recall, the Pass.Value method from the preceding exercise expects an ordinary int parameter, so the following method call will not compile:

int? i = 99; 
Pass.Value(i); // Compiler error

Understanding the properties of nullable types

A nullable type exposes a pair of properties that you can use to determine whether the type actually has a non-null value and what this value is. The HasValue property indicates whether a nullable type contains a value or is null. You can retrieve the value of a non-null nullable type by reading the Value property, like this:

int? i = null; 
... 
if (!i.HasValue) 
{ 
    // If i is null, then assign it the value 99 
    i = 99; 
} 
else 
{ 
    // If i is not null, then display its value 
    Console.WriteLine(i.Value); 
}

In Chapter 4, “Using decision statements,” you saw that the NOT operator (!) negates a Boolean value. The preceding code fragment tests the nullable variable i, and if it does not have a value (it is null), it assigns it the value 99; otherwise, it displays the value of the variable. In this example, using the HasValue property does not provide any benefit over testing for a null value directly. Additionally, reading the Value property is a long-winded way of reading the contents of the variable. However, these apparent shortcomings are caused by the fact that int? is a very simple nullable type. You can create more complex value types and use them to declare nullable variables where the advantages of using the HasValue and Value properties become more apparent. You’ll see some examples in Chapter 9, “Creating value types with enumerations and structures.”