In this article, we will extensively break down the concept of Nullable types in C#. We will discuss the kinds of Nullable types in C#, their characteristics, and how we can use them when building applications.

To download the source code for this article, you can visit our GitHub repository.

To get a clearer view of the Nullable types in C#, let’s start by reviewing the Nullable value types in C#.

What Are Nullable Value Types in C#?

Nullable types are types that can hold a null value alongside their underlying type. C# 2.0, introduced the concept of Nullable types. At that time, we could only declare Nullable value types. In the wake of C# 8.0, we were introduced to Nullable Reference types. They allow us to specify if a reference type can hold a null value or not.

If we were to assign null to a value type:

int number = null;

We would face a compile-time error.

Now, we can bypass this error using a Nullable value type. But, how is this possible?

It turns out that a Nullable value type represents all of the values of its underlying value type T and a null value.

This means we can either assign an int value or a null value to a Nullable int type:

int? number = null;
int? numberTen = 10;

Under the hood, a Nullable value type is an instance of the generic System.Nullable<T> structure. We use it when we need to represent an undefined instance of an underlying value type.

How to Declare Nullable Value Types in C#

We can declare a Nullable value type using the Nullable<T> syntax:

Nullable<bool> areVirusesAlive = null;
Nullable<decimal> futureAccountBalance = null;
Nullable<char> thirtiethLetterOftheAlphabet = null;

Alternatively, we can use a shorthand syntax to declare Nullable value types. This involves appending the ? operator to the underlying value type:

bool? areVirusesAlive = null;
decimal? futureAccountBalance = null;
char? thirtiethLetterOftheAlphabet = null;

These approaches are the same.

How to Assign Values to Nullable Value Types in C#?

C# allows us to assign a value to a variable of a Nullable value type just as we would for its corresponding value type. That’s because a value type is implicitly convertible to its corresponding Nullable value type:

byte? weekDay = 24;

Of course, the beauty of a Nullable value type is that we can also directly assign a null value to it:

byte? weekDay = null;

We must assign a value to a Nullable value type before we use it in a method scope:

void Start()
{
    int? number; 

    Console.WriteLine(number.ToString());
}

Otherwise, a compile-time error occurs.

When we use it as a class field, it is implicitly given a default value of null

public class Sample
{
    int? number; //null by default

    public void Start()
    {
        Console.WriteLine(number.ToString()); //No compile-time error
    }
}

In this case, we do not get a compile-time error because the compiler assigns a default value of null to number.

Access the Underlying Value of a Nullable Value Type in C# 

A Nullable type has various instance-level members that are concerned with its underlying value.

GetValueOrDefault() will return the underlying value for a Nullable value type or it will return null if no value is found:

int result = number.GetValueOrDefault();

The HasValue readonly property of a Nullable value type returns a boolean that tells us if we have assigned it a value or not. When we have not assigned a value to a Nullable value type, its HasValue property is false. Otherwise, it is true:

bool hasValue = number.HasValue;

The Value readonly property will return a value of the underlying type if the Nullable value type has been assigned a value. If we use the Value property to access a Nullable value type that does not have a value:

int value = number.Value;

We will have an InvalidOperationException.

We can avoid this error by using HasValue  to check if the Nullable value type has been assigned a value:

if (number.HasValue)
{
    Console.WriteLine(number.Value);
}

 This check can also be done via a conventional null check:

if (number != null)
{
    Console.WriteLine(number.Value);
}

How to Compare Nullable Value Types in C#

We use the static Nullable.Compare<T> method to compare Nullable value types.

The result of Compare<T> is dependent on the HasValue and Value properties of the variables being compared. It could be equal to zero, less than zero, or greater than zero.

The result is less than zero when the HasValue property for numberOne is false and the HasValue property for numberTwo is true, or when both properties are true and the Value property for numberOne is less than the Value property for numberTwo:

int? numberOne = 10;
int? numberTwo = 12;

Nullable.Compare(numberOne, numberTwo); //-1

It is zero when the HasValue properties for both variables are true or false and the Value for both properties are equal:

int? numberTwo = 12;

Nullable.Compare(numberTwo, numberTwo); // 0

bool? isRunning = null;
bool? isDisposed = null;

Nullable.Compare(isRunning, isDisposed); //0

The result is greater than zero when the HasValue property for numberOne is true and the HasValue property for numberTwo is false, or when both properties are true and the Value property for numberOne is greater than the Value property for numberTwo:

int? numberOne = 10;
int? numberTwo = 12;

Nullable.Compare(numberTwo, numberOne); // 1   

Using Operators With Nullable Value Types in C#

Nullable value types do not have actual unary or binary operators that exist on them. Instead, they make use of lifted operators. Lifted operators are able to operate on Nullable types by “lifting” the operators that exist on their non-nullable counterparts:

int? numberOne = 10;
int? numberTwo = 10;
int? sum = numberOne + numberTwo;

Lifted operators operate by determining if either or both operands are null. If this is the case, the outcome is null. Otherwise, it first unwraps both operands to their non-nullable values and then applies the operator between them. Finally, the result is wrapped back into a Nullable value and returned. 

Using Equality/Inequality Operators

The equality operator returns a boolean when used with Non-Nullable value types.

When we compare Nullable value types that have underlying values of null or we have not assigned values the result is true:

bool? isRunning = null;
bool? isDisposed = null;

bool areEqual = isDisposed == isRunning; //true

If one of the operands is null then we get false:

bool? isActive = true;

bool areEqualTwo = isDisposed == isActive; //false

Otherwise, the values we assigned to the Nullable value types are used for the comparison:

bool? isHuman = false;

bool areEqualThree = isActive == isHuman; //false

This is slightly different when we compare Nullable value types with the inequality operator:

bool? isRunning = null;
bool? isDisposed = null;
bool? isActive = true;
bool? isHuman = false;

bool areEqual = isDisposed != isRunning; //false
bool areEqualTwo = isDisposed != isActive; //true
bool areEqualThree = isActive != isHuman; // true

We see that when both operands are null then our result is false. If one of the operands is null then we get true. Else the values of the Nullable value types are compared.

Comparison Operators

Comparison operators will return false if we set one or both of the operands to null. This means that getting a false value does not necessarily point to one of the values we are comparing being greater than the other:

int? countOne = null;
int? countTwo = null;            

bool areEqual = countOne > countTwo; //false
bool areEqualTwo = countOne < countTwo; //false

For us to properly compare Nullable value types, we need to assign values to them:

int? countThree = 12;
int? countFour = 14;

bool areEqualThree = countFour > countThree; //true

The Null Coalescing Operator

For the null coalescing operator, if the expression on the left-hand side does not evaluate to null, then it returns the left-side value and never checks the right-hand side. If the expression on the left evaluates to null then it will return the value on the right-hand side.

We can use this syntax to assign a value of a Nullable value type to a non-nullable value type:

int? count = null;
long? id = count ?? 1; 

We can also apply the null coalescing assignment operator:

count ??= 13;

What Are Nullable Reference Types in C#

As stated earlier, the distinction between Nullable reference types and Non-Nullable reference types came in C# 8.0.

Originally, reference types were Nullable by default. Because they could hold null and non-null values, they became a leading cause of  NullReferenceException:

string nullableMonth = "November";
string? month = nullableMonth;

Nullable reference types are basically a set of annotations on existing reference types. The compiler uses these annotations to assist us in identifying potential null reference errors.

We can use Nullable reference types optionally as we can turn them on or off.  We can use Nullable reference types by default in .NET 6 project templates. For prior versions of .NET, we will have to manually enable it when required.

How to Enable Nullable Reference Types in C#

We can enable support for Nullable reference types by setting a Nullable context. We can do this either at the project level or at the file level.

From our project file, we can set the Nullable attribute to enable:
<Nullable>enable</Nullable>

Asides from the enable value, the Nullable element in our project file can take four additional values; disable, annotations  and warnings.

When we use enable we are enabling Nullable annotations and also Nullable context compiler warnings in our project.

When we set it to disable, we are disabling Nullable annotations and Nullable context compiler warnings in the project. 

The warnings value will enable Nullable warnings and disable Nullable annotations.

If we do not want the Nullable context to be project-wide, we can use Nullable compiler directives to control specific parts of our project:

#nullable disable

The enable and disable compiler directives work similarly to the Nullable element values. restore on the other hand, will restore all settings to the project settings.

Using the Null Forgiving Operator With Nullable Reference Types

As long we have enabled Nullable reference types within our project, the compiler will be diligent enough to point out possible null references or null dereferencing issues.

When we are sure that a particular expression will never be null, we can apply the null forgiving operator to remove compiler warnings:

class Address
{
    public string Street { get; set; }
}

class User
{
    public Address? Address { get; set; } = null;
    public string? Name { get; set; }
    public int Age { get; set; }

    public void AlertStreet()
    {
        User user = new ();
        Console.WriteLine(user.Address!.Street);
    }
}

We need to be careful when making use of the null forgiving operator. We should never forget that it doesn’t prevent null value-based errors. It will only disable compiler warnings. We should use it only when the compiler is unable to detect that a value is actually non-nullable.

Conclusion

In this article, we discussed the Nullable types in C#. We started out with Nullable value types and lifted operators.  In the later part of this article, we looked at Nullable reference types and how to enable Nullable context at the project level and at the file level. We concluded by looking at how to remove compiler warnings using the null forgiving operator.