Thanks for visiting my blog!
This topic has been on my TODO:
list for quite a while now. As I work with clients, many of them are just ignoring the warnings that you get from Nullable Reference Types. When Microsoft changed to make them the default, some developers seemed to be confused by the need. Here is my take on them:
I also made a Coding Short video that covers this same topic, if you’d rather watch than read:
Before Nullable Reference Types
There has always been two different types of objects in C#: value types
and reference types
. Value types are created on the stack (therefore they go away without needing to be garbage collected); and Reference Types are created by the heap (needing to be garbage collected). Primitive types and structs are value types, and everything else is a reference type, including strings. So we could do this:
int x = 5;
string y = null;
By it’s design, value-types couldn’t be null. They just where:
int x = null; // Error
string y = null;
There were occasions that we needed null on value types. So they introduced the Nullable<T>
struct. Essentially, this allowed you to make value types nullable:
Nullable<int> x = null; // No problem
They did add some syntactical sugar for Nullable<T>
by just using a question mark:
int? x = null; // Same as Nullable<int>
But why nullability? So you can test for whether a value exists:
int? x = null;
if (x.HasValue) Write(x);
While this works, you could test for null as well:
int? x = null;
if (x is not null) Write(x);
OK, this is what Nullable value types are, but reference types already support null. Reference types do support being null, but do not support not allowing null. That’s the difference. By enabling Nullable Reference Types, all reference types (by default) do not support Null unless you use the define them with the question-mark:
object x = null // Doesn't work
But utilizing the null type definition:
object? x = null // works
As C# developers, we spend a lot of time worrying about whether an object is null (since anyone can pass a null for parameters or properties). So, enabling Nullable Reference Types makes that impossible. By default, new projects (since .NET 6) have enabled Nullable Reference Types by default. But how?
Enabling Nullable Reference Types
In C# 8, they added the ability to enable Nullable Reference Types. There are two ways to enable it: file-based declaration or a project level flag. For projects that want to opt into Nullable Reference Types slowly, you can use the file declarations:
#nullable enable
object x = null; // Doesn't work, null isn't supported
#nullable disable
But for most projects, this is done at the project level:
<!--csproj-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
The <Nullable/>
property is what enables the feature.
When you enable this, it will produce warnings for applying null to reference types. But you can even turn these into errors to force a project to address the changes:
<WarningsAsErrors>Nullable</WarningsAsErrors>
Using Nullable Reference Types
So, you’ve gotten this far so let’s talk some basics. When defining a variable, you can opt-into nullability by defining the type with nullability:
string? x = null;
That means anywhere you’re just defining the type (without inferring the type), C# will assume that null isn’t a valid value:
string x = "Hello";
if (x is null) // No longer necessary, this can't be null
{
// ...
}
But what happens when we infer the type? For value types, it is assumed to be a non-nullable type, but for reference type…nullable:
var u = 15; // int
var s = ""; // string?
var t = new String('-', 20); // string?
This is actually one of the reasons I’m moving to the new syntax for creating objects:
object s = new(); // object - not nullable
Not exactly about nullable reference types, but in this case, the object is not null because we’re making sure it’s not nullable.
Classes and Nullable Reference Types
When clients have moved here, the biggest pain they seem to run into is with classes (et al.). After spending so many years writing simple data classes like so:
public class Customer
{
public int Id { get; set;}
public string Name { get; set;} // Warning
public DateOnly Birthdate { get; set;}
public string Phone { get;set;} // Warning
}
Properties that aren’t nullable are expected to be set before the end of the constructor. There are two ways to address make them nullable; and initialize the properties.
Making the properties nullable has the benefit of being more descriptive of the actual usage of the property:
public class Customer
{
public int Id { get; set;}
public string? Name { get; set;} // null unless you set it
public DateOnly Birthdate { get; set;}
public string? Phone { get;set;} // null unless you set it
}
Alternatively, you can set the value:
public class Customer
{
public int Id { get; set;}
public string Name { get; set;} = "";
public DateOnly Birthdate { get; set;}
public string Phone { get;set;} = "";
}
Or,
public class Customer
{
public int Id { get; set;}
public string Name { get; set;}
public DateOnly Birthdate { get; set;}
public string Phone { get;set;}
public Customer(string name, string phone)
{
Name = name;
Phone = phone;
}
}
It may, at first, seem like trouble for certain types of classes. In fact, it’s is not uncommon to opt-out of nullability for entity classes:
#nullable disable
public class Customer
{
public int Id { get; set;}
public string Name { get; set;} // No Warning
public DateOnly Birthdate { get; set;}
public string Phone { get;set;} // No Warning
}
#nullable enable
Testing for Null
When you start using nullable properties on objects, you quickly run into warnings:
Customer customer = new();
WriteLine($"Name: {customer.Name}"); // Warning
The warning is because the compiler can’t confirm it is not null (Name is nullable). This is one of the uncomfortable parts of using Nullable Reference Types. So we can wrap it with a test for null (like you’ve probably been doing for a long time):
Customer customer = new();
if (customer.Name is not null)
{
WriteLine($"Name: {customer.Name}");
}
At that point, the compiler can be sure it’s not null because you tested it. But this seems a lot of work to determine null. Instead we can use some syntactical sugar to shorten this:
Customer customer = new();
WriteLine($"Name: {customer?.Name}"); // Warning
The ?.
is simply a shortcut. If customer
is null, it just returns a null. This allows you to deal with nested nullable types pretty easily:
Customer customer = new();
WriteLine($"Name: {customer.Name?.FirstName?}"); // Warning
In this example, you can see that the ?
is used at multiple places in the code as Name
could be null and FirstName
could also be null.
This also affects how you will allocate a variable that might be null. For example:
Customer customer = new();
string name = customer.Name; // Warning, Name might be null
The null coalescing operator can be used here to define a default:
Customer customer = new();
string name = customer.Name ?? "No Name Specified"; // Warning, Name might be null
The ??
operator allows for the fallback in case of null. which should simplify some common scenarios.
But sometimes we need to help the compiler figure out whether something is null. You might know that a particular object is not null even if it is a nullable property. There is an additional syntax that supports telling the compiler that you know better. Just use the !
syntax.
Customer customer = new();
string name = customer.Name!; // I know it's never null
This just tells the compiler what you expect. If the Name is null, it will throw an exception…so only use it when you’re sure. The bang symbol (e.g. !
) is used at the end of the variable. So if you need to string these, you’ll put the bang at each level:
Customer customer = new();
string name = customer.Name!.FirstName!; // I know they're never null
While using Nullable Reference Types could be seen as a way to over-complicate your code, these bits of syntactical sugar can simplify dealing with nullables.
Generics and Nullable Reference Types
Just like any other code, you can use the question-mark to specify that a value is nullable:
public class SomeEntity<TKey>
{
public TKey? Key { get; set; }
}
The problem with this is that the type specified in TKey
could also be nullable:
SomeEntity<string?> entity = new();
But this results in a warning because you can’t have a nullable of a nullable. The generated type might look like this:
public class SomeEntity<string?>
{
public string?? Key { get; set; }
}
Notice the double question-mark. It also suggests that the generic class doesn’t quite know whether to initialize it or not since it doesn’t know about the nullability. To get around this, you can use the notnull
constraint:
public class SomeEntity<TKey> where : notnull
{
public TKey? Key { get; set; }
}
That way the generic type can be in control of the nullability instead of the caller.
Conclusion
I hope that this quick intro into Nullable Reference Types helps you get your head around the ‘why’ and ‘how’ of Nullable Reference Types. Please comment if you have more questions and/or complaints!