Nullable reference types are one of the significant new features introduced in C# 8.0. The goal is to help developers avoid the most common and frustrating issues in C# development: null reference exceptions, often referred to as the “billion-dollar mistake.”
Before C# 8.0, all reference types were implicitly nullable, meaning that any reference type variable could hold either a reference to an object or a null value. However, this lack of clarity often led to runtime errors when null was assigned to variables or passed around, resulting in null reference exceptions. C# 8.0 introduced nullable reference types to provide a way to explicitly mark variables as nullable or non-nullable, thereby providing better compile-time checks and reducing the likelihood of runtime issues.
This tutorial will guide you through everything you need to know about nullable reference types in C# 8.0 and beyond, explaining how they work, how to enable them, and how to manage them effectively in your codebase.
What Are Nullable Reference Types?
In C# 8.0 and later, nullable reference types allow developers to express whether a reference type can be null or not. The compiler enforces this with warnings and errors where appropriate, helping developers write safer, more robust code.
Nullable reference types are split into two categories:
- Non-nullable reference types: These are the default reference types, which cannot be assigned a
nullvalue. - Nullable reference types: These reference types can hold a
nullvalue.
The idea is simple: if you declare a reference type variable as nullable, the compiler knows that it can potentially hold a null value and will issue warnings if you’re not handling the null case appropriately. If the variable is non-nullable, the compiler will ensure that you’re not assigning null to it or accessing it without initializing it.
How to Enable Nullable Reference Types
Nullable reference types are not enabled by default in C# 8.0+ because they would break backward compatibility with existing codebases. To enable them, you need to do so explicitly at either the project level or within specific files or scopes.
Enabling Nullable Reference Types in the Project File
You can enable nullable reference types for the entire project by modifying the .csproj file. Add the following line within the <PropertyGroup> section:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>Code language: HTML, XML (xml)This setting applies globally to all files in the project.
Enabling Nullable Reference Types in Code
If you want to enable nullable reference types for a specific file or section of code rather than the entire project, you can use the #nullable directive at the top of your C# file:
#nullable enableCode language: C# (cs)You can also disable nullable reference types using:
#nullable disableCode language: C# (cs)If you want to enforce nullable reference types within a particular block of code, you can use #nullable for finer control:
#nullable enable
void MyMethod()
{
string? nullableString = null; // Allowed
string nonNullableString = null; // Warning: CS8600
}
#nullable disableCode language: C# (cs)This flexibility allows you to gradually adopt nullable reference types in a large codebase, starting from new code or specific parts of the application.
Nullable vs. Non-Nullable Reference Types
Once nullable reference types are enabled, the C# compiler starts treating reference types in a new way. Let’s break down the differences between nullable and non-nullable reference types.
Non-Nullable Reference Types
By default, reference types like string, object, List<T>, etc., are non-nullable when nullable reference types are enabled.
#nullable enable
string nonNullableString = null; // Warning: CS8600, null cannot be assigned to a non-nullable reference typeCode language: C# (cs)In this example, the compiler warns you that you are trying to assign null to a non-nullable string. Non-nullable reference types cannot hold a null value. This restriction makes your code safer because you can be confident that whenever you access a non-nullable reference type, it will never be null.
Nullable Reference Types
Nullable reference types are indicated by appending a ? to the reference type. These types can hold a null value.
#nullable enable
string? nullableString = null; // No warningCode language: C# (cs)Here, the compiler understands that nullableString can be null, so it won’t issue a warning. However, this comes with a trade-off: you need to handle null values appropriately when using nullable reference types.
For example:
#nullable enable
void Greet(string? name)
{
if (name != null)
{
Console.WriteLine($"Hello, {name}");
}
else
{
Console.WriteLine("Hello, stranger!");
}
}Code language: C# (cs)In this case, we check whether name is null before using it, ensuring that we don’t run into null reference exceptions at runtime.
Compiler Warnings and Nullability Annotations
When nullable reference types are enabled, the compiler will analyze your code and provide warnings if you’re not handling potential null values correctly. Let’s explore the different kinds of warnings you might encounter.
CS8600: Null Assigned to Non-Nullable Type
This warning occurs when you’re trying to assign a null value to a non-nullable reference type.
#nullable enable
string nonNullableString = null; // Warning: CS8600Code language: C# (cs)CS8602: Dereference of a Possibly Null Reference
This warning occurs when you’re trying to dereference a nullable reference type without first ensuring it’s not null.
#nullable enable
string? nullableString = null;
Console.WriteLine(nullableString.Length); // Warning: CS8602Code language: C# (cs)To fix this, you need to check whether nullableString is null before accessing it:
#nullable enable
string? nullableString = null;
if (nullableString != null)
{
Console.WriteLine(nullableString.Length); // No warning
}Code language: C# (cs)Alternatively, you can use the null-conditional operator (?.) to safely access nullable types:
#nullable enable
string? nullableString = null;
Console.WriteLine(nullableString?.Length); // No warningCode language: C# (cs)In this case, if nullableString is null, the expression will evaluate to null, and nothing will be printed.
CS8603: Null Returned from a Non-Nullable Type
This warning occurs when you’re returning null from a method that has a non-nullable return type.
#nullable enable
string GetNonNullableString()
{
return null; // Warning: CS8603
}Code language: C# (cs)To fix this, you can either change the return type to be nullable or ensure you’re returning a non-null value.
#nullable enable
string? GetNullableString()
{
return null; // No warning
}Code language: C# (cs)Null Forgiving Operator (!)
Sometimes, you may know that a variable is not null but the compiler can’t determine that. In such cases, you can use the null-forgiving operator (!) to suppress the compiler warning.
#nullable enable
string? nullableString = GetString();
Console.WriteLine(nullableString!.Length); // No warningCode language: C# (cs)In this example, nullableString! tells the compiler, “I know this value isn’t null, so don’t warn me about it.” Be careful when using the null-forgiving operator, as it’s easy to introduce runtime errors if you mistakenly assume that a value will never be null.
Adopting Nullable Reference Types in Existing Codebases
Nullable reference types can be a fantastic tool for writing safer code, but adopting them in a large existing codebase can be a bit tricky. Fortunately, C# gives you the tools to adopt nullable reference types incrementally and with care.
Gradual Adoption
You don’t have to enable nullable reference types for your entire project at once. You can enable them in specific files or even in particular sections of code using the #nullable enable directive, as shown earlier.
This approach allows you to gradually refactor your codebase, starting with new code or the most critical areas. Over time, you can enable nullable reference types in more areas as you improve your code’s nullability safety.
Null Annotations and Legacy Code
When working with legacy code that doesn’t have nullability annotations, you’ll need to balance the strictness of nullable reference types with the flexibility of existing code. One option is to disable nullable reference types in legacy files using #nullable disable to prevent compiler warnings from flooding your project.
However, it’s worth considering adding nullability annotations to your existing codebase over time. This process involves updating method signatures, return types, and variable declarations to explicitly mark them as nullable or non-nullable.
Using Annotations for External Libraries
If you’re working with external libraries that haven’t yet adopted nullable reference types, the compiler may not have enough information to provide accurate nullability warnings. In such cases, you can use annotations from the System.Diagnostics.CodeAnalysis namespace to indicate nullability behavior explicitly.
For example, you can use the [NotNull], [MaybeNull], and [AllowNull] attributes to provide hints to the compiler about the nullability of parameters, return types, or properties.
#nullable enable
using System.Diagnostics.CodeAnalysis;
public class Example
{
public void Process([NotNull] string? input)
{
// The compiler assumes 'input' is not null in the method body
Console.WriteLine(input.Length); // No warning
}
}Code language: C# (cs)These annotations help improve nullability checks when working with external code that doesn’t have full nullability annotations.
Nullable Contexts and Annotations in APIs
In addition to enabling nullable reference types in your own code, you can also use nullability annotations to design APIs that clearly communicate nullability expectations. This improves the safety and clarity of your public APIs, making it easier for other developers to use them correctly.
Designing APIs with Nullability Annotations
When designing an API, you can use nullable reference types to make your intentions clear regarding which parameters or return types can be null and which cannot.
For example, consider a simple API for handling user information:
#nullable enable
public class User
{
public string Name { get; set; }
public string? Nickname { get; set; }
public User(string name, string? nickname)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Nickname = nickname;
}
public string? GetNicknameOrDefault()
{
return Nickname ?? "No nickname";
}
}Code language: C# (cs)In this API, Name is a non-nullable property, indicating that every User must have a name. However, Nickname is nullable, reflecting the fact that a user might not have a nickname.
This approach makes the API safer and easier to use, as developers can see immediately whether they need to handle potential null values.
Best Practices for Nullable Reference Types
Nullable reference types are a powerful tool, but they can also introduce complexity if not used carefully. Here are some best practices to follow when working with nullable reference types in your codebase:
1. Enable Nullable Reference Types Gradually
If you’re working with an existing codebase, enable nullable reference types gradually rather than all at once. This allows you to avoid overwhelming your project with warnings and errors and gives you time to refactor your code to handle null values correctly.
2. Use Nullable Reference Types to Express Intent
When designing new APIs or methods, use nullable reference types to clearly express your intent. If a parameter or return value can be null, mark it as nullable. If not, make it non-nullable. This clarity helps other developers (and yourself!) avoid potential null reference exceptions.
3. Always Handle Nullable Values
Whenever you’re working with a nullable reference type, be sure to handle the null case appropriately. Use null checks, the null-conditional operator (?.), or pattern matching to handle nullable values safely.
#nullable enable
string? nullableString = GetString();
if (nullableString is not null)
{
Console.WriteLine(nullableString.Length); // Safe access
}
else
{
Console.WriteLine("String is null");
}Code language: C# (cs)4. Avoid Overusing the Null Forgiving Operator
The null-forgiving operator (!) can be useful in certain situations, but it should be used sparingly. Overusing ! defeats the purpose of nullable reference types, as it disables the safety mechanisms designed to protect your code from null reference exceptions.
5. Refactor Existing Code to Add Nullability Annotations
Over time, consider refactoring your existing codebase to add nullability annotations. This process can be time-consuming, but it helps make your code more robust and easier to maintain in the long run.
Conclusion
Nullable reference types in C# 8.0 and beyond are a powerful feature that helps developers write safer, more robust code by reducing the risk of null reference exceptions. By enabling nullable reference types, you can make your codebase clearer and more explicit about nullability, leading to fewer runtime errors and improved code quality.
In this tutorial, we’ve covered the key concepts of nullable reference types, including how to enable them, the difference between nullable and non-nullable types, how to handle nullable values safely, and best practices for using this feature effectively.
By following these guidelines and using nullable reference types thoughtfully, you can improve your code’s null safety and create more reliable and maintainable applications in C#.
