DEV Community

Cover image for Digging Into Nullable Reference Types in C#
Shawn Wildermuth
Shawn Wildermuth

Posted on • Originally published at wildermuth.com on

Digging Into Nullable Reference Types in C#

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:

https://www.youtube.com/watch?v=tMKcLwlhoEs

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). Primative 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;

Enter fullscreen mode Exit fullscreen mode

By it’s design, value-types couldn’t be null. They just where:

int x = null; // Error
string y = null;

Enter fullscreen mode Exit fullscreen mode

There were occassions 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

Enter fullscreen mode Exit fullscreen mode

They did add some syntactical sugar for Nullable<T> by just using a question mark:

int? x = null; // Same as Nullable<int>

Enter fullscreen mode Exit fullscreen mode

But why nullability? So you can test for whether a value exists:

int? x = null; 

if (x.HasValue) Write(x);

Enter fullscreen mode Exit fullscreen mode

While this works, you could test for null as well:

int? x = null; 

if (x is not null) Write(x);

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

But utilizing the null type definition:

object? x = null // works

Enter fullscreen mode Exit fullscreen mode

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; // Doens't work, null isn't supported
#nullable disable

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

The <Nullable/> property is what enables the feature.

When you enable this, it will produce warnings for appling null to reference types. But you can even turn these into errors to force a project to address the changes:

<WarningsAsErrors>Nullable</WarningsAsErrors>

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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
{
  // ...
}

Enter fullscreen mode Exit fullscreen mode

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?

Enter fullscreen mode Exit fullscreen mode

This is actually one of the reasons I’m moving to the new syntax for creating objects:

object s = new(); // object - not nullable

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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 initalize 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
}

Enter fullscreen mode Exit fullscreen mode

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;} = "";
}

Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Testing for Null

When you start using nullable properties on objects, you quickly run into warnings:

Customer customer = new();

WriteLine($"Name: {customer.Name}"); // Warning

Enter fullscreen mode Exit fullscreen mode

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}");
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

The ?. is simply a shortcut. If Name is null, it jsut returns a null. This allows you to deal with nested nullable types pretty easily:

Customer customer = new();

WriteLine($"Name: {customer?.Name?.FirstName}"); // Warning

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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; }
}

Enter fullscreen mode Exit fullscreen mode

The problem with this is that the type specified in TKey could also be nullable:

SomeEntity<string?> entity = new();

Enter fullscreen mode Exit fullscreen mode

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; }
}

Enter fullscreen mode Exit fullscreen mode

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; }
}

Enter fullscreen mode Exit fullscreen mode

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!

Nullable Reference Types Example

Creative Commons License

This work by Shawn Wildermuth is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.

Based on a work at wildermuth.com.


If you liked this article, see Shawn's courses on Pluralsight.

Top comments (0)