“Brianna dropped the skateboard in front of Sam. “Don’t worry: I won’t let you fall off.” “Yeah? Then why did you bring the helmet?” Brianna tossed it to him. “In case you fall off.” ― Michael Grant, Hunger
Introduction
One of the major sources of unexpected runtime errors is null references.
With nullable reference types you can make the compiler check for null references,
and make it easier for you to avoid the dreaded NullReferenceException
.
.NET 6 will enable nullable reference types by default, so if you did not know about this C# language feature, this is the time to learn about it!
Tony Hoare, the inventor of the null pointer, apologized even for inventing null
pointers 😄
"Null References: The Billion Dollar Mistake"
Enabling null reference types
Nullable reference types are an opt-in feature, so you need to enable this for your project(s).
Open your project's Properties, and then on the build table you can enable Nullable
from the drop-down list.
Or you can do this directly in the Project file:
<Nullable>enable</Nullable>
This will turn on nullable references for your whole project, you can also enable/disable this feature for sections of your code using #nullable
pragma. Using nullable references does not have to be a big-bang change.
You can gradually update your code to support this. So if you have a region of code where you don't want the compiler to check for nullable references, you do this:
#nullable disable
string oldFashioned = null;
Console.WriteLine(oldFashioned);
#nullable enable
Using your first nullable reference
Let's try something. Start by declaring a variable of type String
as follows:
string firstName = null; // warning
Console.WriteLine(firstName);
You will get a compiler warning, saying that you cannot assign null
to a non-nullable type.
With nullable reference types enabled, the compiler will make each reference by default non-nullable,
meaning that you cannot assign a null
value to it.
Now let us add a nullable reference. The syntax is similar to the nullable value type syntax:
string? lastName = null;
Console.WriteLine(lastName);
Now you will not get a warning. That is because the ?
after the type indicates that this reference can be null
.
Nullable reference types use meta-data to allow the compiler do perform a deep check of your references, and to warn you when you are using an unchecked reference, which might result in a runtime error.
There's no runtime difference between a non-nullable reference type and a nullable reference type.
Null forgiving operator
Try to assign a non-nullable reference type to a nullable reference and vice-versa:
lastName = firstName;
firstName = lastName; // warning
The first line will compile without any warnings, because lastName can take a string reference or null
.
The second line however will give you a warning, because firstName can only accept a reference to a string and not null
.
Because lastName
can be null, the compiler emits a warning.
However, we can tell the compiler that we know what we are doing, using the null-forgiving operator !
:
lastName = firstName;
firstName = lastName!; // no warning
Now the assignment does not give us a warning. This is a little like using a cast to tell the compiler that we
are doing this on purpose.
Static Code Analysis
With nullable reference types, the compiler will analyse your code for possible NullReferenceExceptions
and issue warnings. For example we have these two functions:
static string? CanReturnANull(bool returnNull)
=> returnNull ? null : "Hello";
static string CanNotReturnANull()
=> "World!";
When you call CanNotReturnANull
the compiler does emit a warning because it knows this function cannot return a null
value:
string result1 = CanNotReturnANull();
Console.WriteLine(result1.Length);
However, when calling CanReturnANull
the compiler does imit a warning because this function can return a null
value as indicated by the method's return type:
result1 = CanReturnANull(true); // warning
result1 = CanReturnANull(false); // warning
In this case you need to assign the CanReturnANull
result to a nullable reference:
string? result2 = CanReturnANull(true);
Using this result in some expression will again result in a compiler warning because it can cause an exception:
Console.WriteLine(result2.Length);
You can remove the warning by adding a null
check:
if (result2 != null)
{
Console.WriteLine(result2.Length);
}
In this case you can hover the mouse over the result2
variable inside the if
, and it will tell you that result2
is not null here. Hover over result2
outside the if
and it will tell you that result2
can be null here. This the deep analysis at work!
Some applications for nullable reference types
The first reason to enable nullable reference types is less null
checks.
For example a typical method normally needs to check its arguments for null
values:
private static void OldFashioned(string s1)
{
s1 = s1 ?? throw new ArgumentNullException(nameof(s1));
// ...
}
With nullable reference types, there is no need since the compiler will issue a warning.
private static void UsingNullableRefTypes(string s1)
{
// ...
}
Calling this method with possible null
:
UsingNullableRefTypes(result2); // warning
And in places where you should have a null
check it will again warn you.
Nullable reference types also make your code more expressive, it shows intent.
Let us look at the CanNotReturnANull
method again.
This method returns a string
type, without the ?
type modifier.
static string CanNotReturnANull()
=> "World!";
This method says that is will never return a null
. This method signature is honest, I can know that
this method will not return a null
because the type says so. So I don't need to check for null
.
If someone changes this method to return null
, the compiler will issue a warning.
This is especially handy in interfaces, where we can again declare intent through the type.
Same thing for properties. Look at the Person
class. The FirstName
property can not be null
,
while the LastName
property (yes there are countries that do not use a family name) can.
Cultures that don't use family names'
public class Person
{
public string FirstName { get; set; }
public string? LastName { get; set; }
}
Here the compiler will again emit a warning because there is only the default constructor which will leave the FirstName null
. To get rid of it, we need to add a constructor that takes a non-null first name.
public class Person
{
public Person(string firstName, string? lastName = default)
{
this.FirstName = firstName;
this.LastName = lastName;
}
public string FirstName { get; set; }
public string? LastName { get; set; }
}
Using Nullability Attributes
Sometimes you might want to help the compiler by explicitly defining the nullability of a variable.
AllowNull and DisallowNull
For example, you might want to have a property that returns a non-null default value when its value is null
.
public class Product
{
public int Id { get; set; }
private string? displayName = null;
public string DisplayName
{
get => displayName ?? this.Id.ToString();
set => displayName = value;
}
}
This display property checks if the displayName
field is null
and returns a non-null value if so.
So we declare this property to be non-nullable string
.
However, we need the ability to reset the display name to null
so it will use the fallback behavior.
If we do so, we get a compiler warning:
Product product = new Product();
product.DisplayName = "Choco";
product.DisplayName = null; // warning
We can tell the compiler that this is ok by adding the [AllowNull]
attribute to the property.
[AllowNull]
public string DisplayName
{
get => displayName ?? this.Id.ToString();
set => displayName = value;
}
Now the compiler will no longer emit a warning:
product.DisplayName = null; // no warning
In a similar way we can use the [DisallowNull]
attribute for a property that can return null
,
but may never be set to null
.
NotNull
With the NotNull
attribute you can tell the compiler that an argument will be non-null after the call.
Say that you need to method that will create an instance of a class like this:
private static void Initialize<T>(int id, [NotNull] ref T? result) where T : class
{
Type t = typeof(T);
result = Activator.CreateInstance(t, args: id) as T;
}
You can call this method with a null
reference, the compiler will deduce that after this method
(should it return instead of throwing an exception) the by-ref argument is non-null.
That is why the Console.WriteLine
will not issue a warning. You can verify this by hovering
over the product2
variable.
Product? product2 = null;
Initialize<Product>(1, ref product2);
Console.WriteLine(product.DisplayName);
You could use this to ensure that a reference is not null
, for example we will use an extension
method that throws whenever a reference is null
. By applying the NotNull
attribute we tell the
compiler that after this method returns we are sure that the reference is not null
:
private static T IsNotNull<T>([NotNull] this T? x) where T : class
{
if( x is null )
{
throw new ArgumentNullException();
}
return x;
}
We can now use this as follows without warnings:
Product? product3 = CreateProduct();
Console.WriteLine(product3.IsNotNull().DisplayName);
This is a very common scenario, so C# has the null-forgiving operator !
. Although product3
can be null
,
we can tell the compiler that we are sure that it is not null
, so we don't get warnings using it.
Product? product3 = CreateProduct();
Console.WriteLine(product3!.DisplayName);
What happens when product3
is null
? Then we will get a NullReferenceException
!
MaybeNullWhen
Sometimes you may want to return a reference from a method which can be null depending on circumstances.
For example, add the TryParse
method to the Product
class:
public static bool TryParse(string info, out Product? p)
{
if (info.StartsWith("P"))
{
if (int.TryParse(info.Substring(1), out int id))
{
p = new Product(id);
return true;
}
}
p = null;
return false;
}
When we use this method to parse a string into a product we will still get a warning, even when the TryParse
method is successful:
string productInfo = "P123";
if (Product.TryParse(productInfo, out Product? someProduct))
{
Console.WriteLine(someProduct.Id); // someProduct may be null here
}
We can tell the compiler that the TryParse
method will return a null
reference when the result of the method is false
but not when the result is true
using the MaybeNullWhen
attribute as follows:
public static bool TryParse(string info, [MaybeNullWhen(false)] out Product p)
{
if (info.StartsWith("P"))
{
if (int.TryParse(info.Substring(1), out int id))
{
p = new Product(id);
return true;
}
}
p = null;
return false;
}
string productInfo = "P123";
if (Product.TryParse(productInfo, out Product? someProduct))
{
Console.WriteLine(someProduct.Id); // no more warning
}
Other attributes exist, check out MemberNotNullWhenAttribute
or NotNullWhen
...
Using Non-Nullable reference types with simple classes
Sometimes you need a class that has non-nullable properties but you can't have
a single constructor to initialize all properties. Entity Framework comes to mind since materialization can use the default constructor to create the instance and then the property setters to initialize the instance's properties.
For example, the Category
class will give you a warning for the CategoryName
property:
namespace NullableReferenceTypes;
public class Category
{
public int Id { get; set; }
public string CategoryName { get; set; } // warning
}
You can fix this using the default!
syntax. Here we assign a property to its
default
value, and using the null-forgiving operator we can remove the warning:
namespace NullableReferenceTypes;
public class Category
{
public int Id { get; set; }
public string CategoryName { get; set; } = default!;
}
Summary
With nullable reference types you can make the compiler do a lot of the work for you. Let the compiler
find potential null-references, instead of finding them at runtime, especially in production!