The term Nullable Reference Types refers to a set of features introduced in C# 8.0 that help us to write cleaner, more robust code that is resistant to unintended null dereferencing. The features that are provided make us aware of possible null reference exceptions that could occur in our applications, reducing the likelihood of encountering the dreaded NullReferenceException
at runtime.
However, as wonderful as this safety net is, we may occasionally find ourselves tangled in a web of compiler warnings about potential nulls. So how can we keep our code clean and concise while still benefiting from these new features?
In this article, I will walk through some code examples and provide practical tips along the way to help remove some of the pain. Let’s dive in!
Understanding Nullable Reference Types
Nullable reference types can be used in C# version 8.0 or greater, allowing us to express when a reference type should allow null to be assigned to it. This brings reference types closer in nature to value types, in that you can now have both nullable and non-nullable reference types.
Consider the following simple example.
string? nullableString = null; // Allowed. string nonNullableString = null; // Compiler Warning. Console.WriteLine(nullableString); Console.WriteLine(nonNullableString);
In the above code example, the nullableString
variable is declared as ‘nullable’ by appending the ?
operator after the type, before the variable name. The nonNullableString
variable is considered to be ‘non-nullable’ in this example when nullable reference types are enabled.
Note that the Console.WriteLine
methods calls have been added to prevent compiler warnings about unused variables.
With nullable reference types enabled, the compiler assumes that reference types are non-nullable by default and will issue warnings when it detects possible null dereferences or possible null assignments to non-nullable variables.
Enabling Nullable Reference Types
While the features provided by nullable reference types are optional, they are enabled by default when you create new projects that are using a new enough C# version. In practice, this means projects that are targeting .NET 6 or greater.
If you are working with an existing project that doesn’t have nullable reference types enabled already, the first step towards using nullable reference types effectively is to enable them for your project. Assuming that you have already upgraded your project to a new enough C# version (usually this happens as a result of upgrading your project to a newer .NET version) you can enable nullable reference types by adding the following to your project (.csproj) file.
<PropertyGroup> <Nullable>enable</Nullable> </PropertyGroup>
Note that you should already have an existing PropertyGroup
element within your project file, so I recommend that you add the Nullable
element to it instead of creating a new group of properties.
There are other possible values for the Nullable
element such as warnings
and annotations
. You can find additional information regarding these on the Microsoft Docs. However, the enable
value is what you will usually want to go with.
Alternatively, it is possible to enable nullable reference types at a more granular level by adding #nullable enable
to the top of your .cs file, as shown below.
#nullable enable string? nullableString = null;
Console.WriteLine(nullableString);
Note that you can also intersperse #nullable enable
and #nullable disable
throughout a .cs file to enable or disable nullable reference types in specific regions of your code.
Wrangling Compiler Warnings
Now that you have nullable reference types enabled, what happens if you’re faced with a multitude of compiler warnings? What approaches can be taken to rid ourselves of the null reference warnings?
Consider the following code example.
var user = new User { Name = "Jonathan" }; string userName = GetUserName(user); string GetUserName(User user) { return user.Name; } public class User { public string Name { get; set; } }
For the above example, the following compiler warning will be raised regarding the Name
property of the User
class.
Non-nullable property ‘Name’ must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
If we are treating compiler warnings as errors (as we should) this leaves us with a few options for resolving the issue which are covered in the following subsections.
Nullable property
The first thing we could consider doing is to implement what the compiler suggests.
We could declare the Name
property as nullable, as follows.
public class User { public string? Name { get; set; } }
However, the problem with this approach is that we are losing the benefits of having reference types that should not be null. Although in some cases we’ll need to make our reference types nullable, ideally, we should prefer them to be non-nullable where possible.
The other problem with this approach is that the compiler warning has now shifted to the line of code within the GetUserName
method, where the compiler warns of a possible null reference return (‘Name’ may be null here).
Null-coalescing check
To fix the possible null reference return warning, we could update the GetUserName
method as follows.
string GetUserName(User user) { return user.Name ?? ""; }
In this updated code, we are using the null-coalescing operator (??
) which will check if the Name
property is null and will fall back to an empty string if required. By doing this we ensure there is no possibility of a null value being returned by the method.
While this works and is a good approach for removing the compiler warning, it does mean that we would potentially need to add this sort of check to lots of places across our code base. We need to be wary of things getting messy, especially if the Name
property is accessed in many places, or if lots of other nullable properties need to be added to the User
class.
Null-conditional check
If our GetUserName
method happened to feature a nullable User
parameter (i.e. User?
) we can make our code more robust by adding a null-conditional operator check, as shown below.
string GetUserName(User? user) { return user?.Name ?? ""; }
By adding the null-conditional operator (?
) after the user
variable, before we attempt to access the Name
property and complementing this with the null-coalescing operator (??
) operator, we can guard against both null possibilities and return an empty string if either user
or Name
is null.
Null-forgiving
Another (usually less appealing) option to work around the issue is to use the null-forgiving operator (!
).
The null-forgiving operator can be used for cases where we know for sure that a value won’t be null. We can tell the compiler this by using a !
character or as it’s sometimes humorously known, the “bang” or “dammit” operator, as shown below.
string GetUserName(User user) { return user.Name!; }
In the above example, we are using the null-forgiving operator (!
) to essentially tell the compiler “It’s ok, I know this will never be null”. While doing this removes the compiler warning, as you can imagine, this is a dangerous assumption to make in most cases, so the null-forgiving operator should be used sparingly to avoid runtime exceptions.
Don’t just slap a !
character into your code every time you see a nullable reference compiler warning!
Default value
The other (and the simplest) thing we could do is to resolve the warning from the original User
code example is to initialise the Name
property with a non-null default value.
public class User { public string Name { get; set; } = ""; }
In this scenario, the Name
property will always start with a non-null value; therefore, there is no possibility of it being null unless it has been set to null by subsequent code.
If there is a default value that makes sense for a specific property, this is the easiest route to avoid lots of compiler warnings and removes a lot of the burden from the code that needs to work with a User
object.
Null-State Attributes
Another powerful and lesser-known tool that you can utilise in relation to nullable reference types is the null-state attributes which are documented below.
Attribute | Category | Meaning |
---|---|---|
AllowNull |
Precondition | A non-nullable parameter, field, or property may be null. |
DisallowNull |
Precondition | A nullable parameter, field, or property should never be null. |
MaybeNull |
Postcondition | A non-nullable parameter, field, property, or return value may be null. |
NotNull |
Postcondition | A nullable parameter, field, property, or return value will never be null. |
MaybeNullWhen |
Conditional postcondition | A non-nullable argument may be null when the method returns the specified bool value. |
NotNullWhen |
Conditional postcondition | A nullable argument won’t be null when the method returns the specified bool value. |
NotNullIfNotNull |
Conditional postcondition | A return value, property, or argument isn’t null if the argument for the specified parameter isn’t null. |
MemberNotNull |
Method and property helper methods | The listed member won’t be null when the method returns. |
MemberNotNullWhen |
Method and property helper methods | The listed member won’t be null when the method returns the specified bool value. |
DoesNotReturn |
Unreachable code | A method or property never returns. In other words, it always throws an exception. |
DoesNotReturnIf |
Unreachable code | This method or property never returns if the associated bool parameter has the specified value. |
Null-State Attributes (Source: Microsoft Docs)
These attributes allow you to annotate your properties, methods, parameters etc. with additional metadata that helps the compiler understand when a value may or may not be null.
NotNullAttribute example
As an example, let’s say we update the GetUserName
method as follows to return a nullable string.
var user = new User { Name = "Jonathan" }; string userName = GetUserName(user); string? GetUserName(User? user) { return user?.Name ?? ""; }
With this change in place, the compiler will now display a warning where the GetUserName
method is called (Converting null literal or possible null value to non-nullable type).
While it is a contrived example, in this particular case, we are sure that our method will never return null. With the null-conditional and null-coalescing checks that are in place, even though the return type is a nullable string we are ensuring that a non-null string value is always returned.
Given that the GetUserName
method cannot return null, we can decorate the method with the NotNullAttribute
using the syntax shown in the example below to indicate that this applies to the value returned by the method.
using System.Diagnostics.CodeAnalysis; var user = new User { Name = "Jonathan" }; string userName = GetUserName(user); [return: NotNull] string? GetUserName(User? user) { return user?.Name ?? ""; }
With the above change in place, the compiler warning will be removed.
NotNullWhenAttribute example
A practical example of one of the null-state attributes being used by the framework can be seen in the following example.
public static bool IsNullOrWhiteSpace([NotNullWhen(false)] string? value) { if (value == null) return true; for (int i = 0; i < value.Length; i++) { if (!char.IsWhiteSpace(value[i])) return false; } return true; }
The above example contains the implementation of the .NET string.IsNullOrWhiteSpace
method.
The method accepts a nullable string
parameter which is decorated with the NotNullWhen
attribute. This tells the compiler that if the method returns false then the parameter value that was passed to it can be considered to be non-null.
This is what allows code like the following to compile without warnings.
string? userName = GetUserName(user); if (!string.IsNullOrWhiteSpace(userName)) { Console.WriteLine(userName.ToString()); }
In the above example, if the string.IsNullOrWhiteSpace
method did make use of the NotNullWhen
attribute, the compiler would warn of a possible null dereference where userName.ToString()
is called.
I encourage you to review where it might make sense to use null-state attributes to help simplify code that calls/accesses your methods and properties.
To Null or Not To Null
Nullable reference types are a powerful tool that can make our code safer. But remember, the goal here isn’t to eliminate nulls entirely. The aim of nullable reference types is to allow us to write more robust code that is aware of nulls and to enable us to make good decisions such that we can avoid null reference exceptions.
To sum it all up, I recommend that you embrace nullable reference types, listen to what the compiler warnings are telling you, and use these tools to help you express your intent more clearly.
Comments