Understanding nullability
If you're a .NET developer, chances are you've encountered the System.NullReferenceException. This occurs at run time when a null
is dereferenced; that is, when a variable is evaluated at runtime, but the variable refers to null
. This exception is by far the most commonly occurring exception within the .NET ecosystem. The creator of null
, Sir Tony Hoare, refers to null
as the "billion-dollar mistake."
In the following example, the FooBar
variable is assigned to null
and immediately dereferenced, thus exhibiting the problem:
// Declare variable and assign it as null.
FooBar fooBar = null;
// Dereference variable by calling ToString.
// This will throw a NullReferenceException.
_ = fooBar.ToString();
// The FooBar type definition.
record FooBar(int Id, string Name);
The problem becomes much more difficult to spot as a developer when your apps grow in size and complexity. Spotting potential errors like this is a job for tooling, and the C# compiler is here to help.
Defining null safety
The term null safety defines a set of features specific to nullable types that help reduce the number of possible NullReferenceException
occurrences.
Considering the previous FooBar
example, you could avoid the NullReferenceException
by checking if the fooBar
variable was null
before dereferencing it:
// Declare variable and assign it as null.
FooBar fooBar = null;
// Check for null
if (fooBar is not null)
{
_ = fooBar.ToString();
}
// The FooBar type definition for example.
record FooBar(int Id, string Name);
To aid in identifying scenarios like this, the compiler can infer the intent of your code and enforce the behavior desired. However, this is only when a nullable context is enabled. Before discussing nullable context, let's describe the possible nullable types.
Nullable types
Before C# 2.0, only reference types were nullable. Value-types such as int
or DateTime
couldn't be null
. If these types are initialized without a value, they fall back to their default
value. In the case of an int
, this is 0
. For a DateTime
, it's DateTime.MinValue
.
Reference types instantiated without initial values work differently. The default
value for all reference types is null
.
Consider the following C# snippet:
string first; // first is null
string second = string.Empty // second is not null, instead it's an empty string ""
int third; // third is 0 because int is a value type
DateTime date; // date is DateTime.MinValue
In the preceding example:
first
isnull
because the reference typestring
was declared but no assignment was made.second
is assignedstring.Empty
when it's declared. The object never had anull
assignment.third
is0
despite not being assigned. It's astruct
(value-type) and has adefault
value of0
.date
is uninitialized, but itsdefault
value is System.DateTime.MinValue.
Starting with C# 2.0, you could define nullable value types using Nullable<T>
(or T?
for shorthand). This allows value-types to be nullable. Consider the following C# snippet:
int? first; // first is implicitly null (uninitialized)
int? second = null; // second is explicitly null
int? third = default; // third is null as the default value for Nullable<Int32> is null
int? fourth = new(); // fourth is 0, since new calls the nullable constructor
In the preceding example:
first
isnull
because the nullable value type is uninitialized.second
is assignednull
when it's declared.third
isnull
as thedefault
value forNullable<int>
isnull
.fourth
is0
as thenew()
expression calls theNullable<int>
constructor, andint
is0
by default.
C# 8.0 introduced nullable reference types, where you can express your intent that a reference type might be null
or is always non-null
. You may be thinking, "I thought all reference types are nullable!" You're not wrong, and they are. This feature allows you to express your intent, which the compiler then tries to enforce. The same T?
syntax expresses that a reference type is intended to be nullable.
Consider the following C# snippet:
#nullable enable
string first = string.Empty;
string second;
string? third;
Given the preceding example, the compiler infers your intent as follows:
first
is nevernull
as it is definitely assigned.second
should never benull
, even though it's initiallynull
. Evaluatingsecond
before assigning a value results in a compiler warning as it is uninitialized.third
might benull
. For example, it might point to aSystem.String
, but it might point tonull
. Either of these variations are acceptable. The compiler helps you by warning you if you dereferencethird
without first checking that it isn't null.
Important
In order to use the nullable reference types feature as shown above, it must be within a nullable context. This is detailed in the next section.
Nullable context
Nullable contexts enable fine-grained control for how the compiler interprets reference type variables. There are four possible nullable contexts:
disable
: The compiler behaves similarly to C# 7.3 and earlier.enable
: The compiler enables all null reference analysis and all language features.warnings
: The compiler performs all null analysis and emits warnings when code might dereferencenull
.annotations
: The compiler doesn't perform null analysis or emit warnings when code might dereferencenull
, but you can still annotate your code using nullable reference types?
and null-forgiving operators (!
).
This module is scoped to either disable
or enable
nullable contexts. For more information, reference Nullable reference types: Nullable contexts.
Enable nullable reference types
In the C# project file (.csproj), add a child <Nullable>
node to the <Project>
element (or append to an existing <PropertyGroup>
). This will apply the enable
nullable context to the entire project.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- Omitted for brevity -->
</Project>
Alternatively, you can scope nullable context to a C# file using a compiler directive.
#nullable enable
The preceding C# compiler directive is functionally equivalent to the project configuration, but it's scoped to the file in which it resides. For more information, see Nullable reference types: Nullable contexts (docs)
Important
The nullable context is enabled in the .csproj file by default in all C# project templates starting with .NET 6.0 and greater.
When the nullable context is enabled, you'll get new warnings. Consider the previous FooBar
example, which has two warnings when analyzed in a nullable context:
The
FooBar fooBar = null;
line has a warning on thenull
assignment: C# Warning CS8600: Converting null literal or possible null value to non-nullable type.The
_ = fooBar.ToString();
line also has a warning. This time the compiler is concerned thatfooBar
may be null: C# Warning CS8602: Dereference of a possibly null reference.
Important
There is no guaranteed null safety, even if you react to and eliminate all the warnings. There are some limited scenarios that will pass the compiler's analysis, yet result in a runtime NullReferenceException
.
Summary
In this unit, you learned to enable a nullable context in C# to help guard against NullReferenceException
. In the next unit, you'll learn more about explicitly expressing your intent in a nullable context.