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 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.