7

Nullable Reference types in C# – Best practices

 3 years ago
source link: https://www.dotnetcurry.com/csharp/nullable-reference-types-csharp
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Nullable Reference types in C# – Best practices

In recent years, there’s been a trend of strict null checking in programming languages:

  • TypeScript has the strictNullChecks option.
  • In Kotlin (preferred language for Android development), all types don’t allow null values by default.
  • In Swift (Apple’s language of choice), only the Optional data type allows null values.

Nullable reference types bring similar functionality to C#. The feature was originally planned for C# 7 and was finally released as part of C# 8.

Using nullable reference types in C#

Since the introduction of generics in C# 2, value types can be declared nullable or non-nullable:

int nonNullable = null; // compiler error
int? nullable = null;

The int? notation is a shorthand for the Nullable<int> generic type which wraps a value type to allow assigning a null value to it.

In C# 8, nullable reference types use the same syntax to give the option of declaring reference types as nullable (i.e. allowing a null value) or non-nullable (not allowing a null value):

string nonNullable = null; // compiler warning
string? nullable = null;

Because of the language history, the decision to use the same syntax for value types and reference types changes the behavior of the language for reference types. Before C# 8, all reference types were nullable. They were however declared without a question mark:

// before C# 8
string nullableString;
// since C# 8
string nonNullableString;

To avoid such a breaking change in a new version of the language, the nullable reference types is the only feature of C# 8 that isn’t enabled by default. An explicit opt-in is required for each project. The following property must be added to the project file for that:

<Nullable>enable</Nullable>

In recent versions of Visual Studio 2019, the option is also available on the Build page of the Project Properties window:’’

csharp-nullable-option-project-properties

Figure 1: Nullable option on the Build page of the Project Properties window

The option is only available for projects using C# 8 or a later version. Different from the previous versions of the language, the support for C# 8 wasn’t added to older .NET runtimes. The language is only supported with .NET Standard 2.1 and compatible runtimes: .NET Core 3.1 or newer, latest versions of Xamarin and Mono, and potentially future versions of UWP and Unity. There is no official support for any version of the .NET framework.

Are you a .NET, C#, Cloud or Web Developer looking for a resource covering New Technologies, in-depth Tutorials and Best Practices?

Well, you are in luck! We at DotNetCurry release a FREE digital magazine once every few months aimed at Developers, Architects and Technical Managers. This magazine covers ASP.NET Core, Xamarin, C#, Patterns and Practices, .NET Core, ASP.NET MVC, Azure, DevOps, ALM, TypeScript, Angular, React, Vuejs and much more.

Subscribe to this magazine for FREE and receive the current and upcoming editions, right in your Inbox. No Spam Policy.

Click here to Download the Latest Edition For Free

Static code analysis

There is an important difference in how null checking is implemented for value types and reference types. While assigning a null value to a non-nullable value type causes a compiler error, doing the same with a non-nullable reference type will only result in a warning. You can still treat it as an error using the Treat warnings as errors compiler option.

The static analysis will report warnings at compile time when a NullReferenceException might be thrown at run time, for example:

When assigning a literal null value to a non-nullable variable:

string nonNullable = null

When assigning a nullable variable to a non-nullable variable:

string? nullable = null;
string nonNullable = nullable;

When accessing members of a nullable variable:

var length = nullable.Length;

Of course, the warning won’t be reported if you check for null before doing the potentially dangerous operation:

private int GetLength(string? nullable)
{
if (nullable == null)
{
return 0;
}
return nullable.Length;
}

However, the static analysis is not perfect. Even if you get rid of all the warnings, there is still a possibility of a NullReferenceException being thrown at run time:

public class Person
{
public Person(string firstName, string lastName, string? homeCountry = null)
{
FirstName = firstName;
LastName = lastName;
HomeCountry = homeCountry;
}
public string FirstName { get; set; }
public string LastName { get; set; }
public string? HomeCountry { get; set; }
public void Relocate(string? country = null)
{
HomeCountry = country;
}
}
var person = new Person("John", "Doe", "Unknown");
if (person.HomeCountry != null)
{
person.Relocate(); // sets HomeCountry to null
var countryLength = person.HomeCountry.Length; // no warning
}

Changes to objects in a different context (another method, different thread) are not detected. Although the above example is contrived, similar situations might occur in real code as it grows more complex.

Even if not all potential run-time exceptions are detected at compile time, there’s still value in those that are. Each one of them could be a nasty bug that’s now easy to fix.

The #nullable directive

When you enable the nullable reference types feature in a new project, fixing the warnings that appear as you write the code won’t be a big deal most of the time.

On the other hand, if you enable the feature in an existing project with a lot of code, there is a strong possibility that an overwhelming number of warnings will be reported immediately. Fixing all of these might take a while.

To make the transition easier, a new #nullable directive has been introduced into the language. It can be used to enable or disable the nullable reference types feature inside a single source code file. For example, the feature can be fully disabled in a file if you put the following at the top of it:

#nullable disable

The directive supports three commands:

  • enable enables the feature.
  • disable disables the feature.
  • restore reverts the feature to the project level setting.

Instead of enabling or disabling the feature in full, only part of it can be affected by adding another keyword at the end of the directive:

  • warnings enables or disables the warnings emitted by static code analysis.
  • annotations enables or disables support for declaring nullable or non-nullable reference types.

Hence, the following directive in a project with nullable reference types enabled will disable any warnings in the rest of the file (from the point where it is placed onward) but leave the ability to declare nullable or non-nullable reference types:

#nullable disable warnings

In my opinion, the #nullable directive can make it much more difficult to fully understand the behavior of the code in a project because the same exact code can have a different meaning based on the #nullable directives in the file:

#nullable enable annotations
string doesntAllowNulls;
#nullable disable annotations
string allowsNulls;

It’s difficult enough to switch between projects with the nullable reference feature enabled or disabled. Dealing with code in the same project (or even the same file) exhibiting different behavior because of the #nullable directive raises the complexity even more.

My suggestion is to avoid the #nullable directive as much as possible. While introducing nullable reference type into an existing codebase, you might need to place the directive at the top of some files with too many warnings. But as soon as possible you should remove the directives and fix those warnings instead.

Nullable reference types in libraries

To fully benefit from nullable reference types, it’s important that any class libraries referenced by the project are also annotated for nullable reference types, i.e. they specify the nullability of reference types acting as their inputs and outputs.

Unfortunately, more than a year after the initial release of the feature that’s not a given. Even in the .NET 5 base class library, only 94% of the assemblies are fully annotated for nullable reference types. The plan is to cover the remaining assemblies before the release of .NET 6 in November 2021. Of course, the percentage of third-party libraries with annotations for nullable reference types is even lower. The reason for that is additional work needed to add hese annotations.

One aspect of this is the fact that it’s in the best interest of library maintainers for their libraries to maintain compatibility with .NET Standard 2.0 (and consequently the .NET framework) even after implementing this C# 8 specific feature that depends on .NET Standard 2.1.

Fortunately, there is an officially supported way to add annotations for nullable reference types to a .NET Standard 2.0 library by manually adding the following property to the project file to enable the use of C# 8:

<LangVersion>8.0</LangVersion>

There are still some C# 8 features that won’t work in such a library (asynchronous streams, for example). But most importantly, nullable reference types will be fully supported. Such a library will still work with the .NET framework as if nullable reference types weren’t used in it. But any .NET Standard 2.1 compatible projects (e.g. .NET Core 3.1. or Xamarin) will get full information about the nullability of reference types.

Let’s look at the following code snippet using the well-known Json.NET library to see how beneficial this can be:

var player = JsonConvert.DeserializeObject<Player>(null);

The library introduced annotations for nullable reference types in version 12. With version 11, there will be no warning for the above line of code even if nullable reference types are enabled in the consuming project. Still, at run time the code will throw an ArgumentNullException. However, with version 12, the same line of code will result in a warning at compile time making it easier to detect and fix the bug.

However, even in version 12, code analysis won’t detect all potential NullReferenceExceptions thrown at run time. Let’s look at the next two lines of code, for example:

Player player = JsonConvert.DeserializeObject<Player>("null");
var username = player.Username;

In the first line, a null value will be assigned to a non-nullable reference type but there will be no compile-time warning. Neither will there be one in the second line, although a NullReferenceException will be thrown at run time.

The only way to get a compile-time warning would be to modify the first line of code as follows:

var player = JsonConvert.DeserializeObject<Player?>("null");
var username = player.Username;

Now, there will be a warning in the second line of code when trying to access a member of player without first testing its value for null. The question is, will you always use a nullable type as the generic type argument if the compiler doesn’t warn you about it?

Annotation attributes in .NET Standard 2.1

There is a way to add such warnings, though. But not in a .NET Standard 2.0 library. .NET Standard 2.1 adds a new generic constraint and a set of annotation attributes to describe the use of nullable reference types in more detail.

We will use some of them to describe the following wrapper for the Json.NET DeserializeObject method from above:

public static T DeserializeObject<T>(string json)
{
return JsonConvert.DeserializeObject<T>(json);
}

First, we can add a constraint for the generic type argument T. Unfortunately, there’s no way to require it to be a nullable reference type. So instead, we can require it to be a non-nullable reference type:

public static T DeserializeObject<T>(string json) where T: notnull
{
return JsonConvert.DeserializeObject<T>(json);
}

While this constraint can be useful in certain scenarios, it doesn’t solve the initial problem of the missing warning. Even worse. It prevents the consuming code from using a nullable reference type as the generic type argument to get the compiler warning. But it does ensure that the type argument will always be non-nullable. In combination with an appropriate annotation attribute, the final goal of having a compile time warning can still be achieved:

[return: MaybeNull]
public static T DeserializeObject<T>(string json) where T: notnull
{
return JsonConvert.DeserializeObject<T>(json);
}

When applied to the return value, the MaybeNull attribute specifies that the return type value will be a nullable reference type even if the generic type argument is a non-nullable reference type. Because of that, failing to do a null check before accessing the members of the returned value or assigning that value to a non-nullable variable will result in a compile-time warning.

This is just one of the available annotation attributes for describing the reference type nullability in more detail. They can be grouped in two categories:

  • Describing the output values: MaybeNull and NotNull have the exact opposite meaning. Conditional variations for both are also available: MaybeNullWhen, NotNullWhen, and NotNullIfNotNull.
  • Describing the input values: AllowNull and DisallowNull.

Examples of use for each attribute are available in the official documentation.

Nullable Reference Type Support in Entity Framework Core

Of all libraries with support for nullable reference types, Entity Framework Core might be the one with the most impact on the code you write. All the specifics are well documented. I’m just going to point out the most important parts that you need to be aware of.

In the DbContext class, the DbSet properties for individual tables should be non-nullable because the base class constructor will ensure that they are always initialized. However, the following line of code will result in a compile-time warning due to uninitialized non-nullable property:

public DbSet<Player> Players { get; set; }

To tell the compiler that the value is initialized without initializing it yourself, the null-forgiving operator ! can be used:

public DbSet<Player> Players { get; set; } = null!;

The modified line of code now initializes the property with the null value which on its own would still cause a warning. The null-forgiving operator tells the compiler to ignore that warning.

Although the null-forgiving operator can be used anywhere to override the static code analysis findings, it shouldn’t be abused unless you’re certain that the code analysis is mistaken, and you know better. Otherwise you’ll just get an exception at run time instead of the compile-time warning.

In combination with Entity Framework Core, the null-forgiving operator is also useful for non-nullable navigation properties in entity classes which are again initialized by the library code:

public Country HomeCountry { get; set; } = null!;

The regular non-nullable properties should be initialized in the only public class constructor:

public Player(int id, string username, string emailAddress)
{
Id = id;
Username = username;
EmailAddress = emailAddress;
}

This is important because entity classes are also instantiated in code and that’s the only way to ensure that all properties are initialized. For records from the database, Entity Framework Core would initialize the properties anyway by assigning values directly to them. But it can also use the constructor for that when present.

The final and probably most important detail to be aware of is the meaning of non-nullable reference types for properties. Just like non-nullable value types, they result in a non-nullable database column. This means that the Required attribute is not needed anymore. Instead, the type of the property must be either nullable or non-nullable.

Special care must be taken when introducing nullable reference types in an existing codebase. Look at the following property:

public Country HomeCountry { get; set; }

Before nullable reference types, it would mean a nullable database column because there’s no Required attribute on it. After enabling the nullable reference types, the database column will become non-nullable because the same syntax now means a non-nullable type. To keep the same database model, the type should be changed to a nullable reference type:

public Country? HomeCountry { get; set; }

Fortunately, when generating a new migration, there will be a warning because of the column data type change in case you forget to make this modification in code. Still, it requires you to pay enough attention and review the generated migration. But you should be doing that anyway.

Conclusion

In this article, I looked at the experience of using nullable reference types one year after the initial release. I started with the basics, describing how the functionality works. I continued with a closer look at the #nullable directive as a tool for incremental introduction of nullable reference types into existing code. I explained how class libraries can provide information about nullable reference types to the consuming code and concluded with a closer look at support for nullable reference types in Entity Framework Core.

Although using nullable reference types can introduce its own set of problems, I still think it’s beneficial because it helps you find potential bugs and allows you to better express your intent in the code. For new projects, I would recommend you enable the feature and do your best to write code without warnings. It shouldn’t be too difficult, and your code will be better because of it. Enabling the feature in existing code will be more challenging and might not be worth it, especially if you don’t plan to do much new development.

This article was technically reviewed by Yacoud Massad.

This article has been editorially reviewed by Suprotim Agarwal.

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

Was this article worth reading? Share it with fellow developers too. Thanks!
Author
Damir Arh has many years of experience with software development and maintenance; from complex enterprise software projects to modern consumer-oriented mobile applications. Although he has worked with a wide spectrum of different languages, his favorite language remains C#. In his drive towards better development processes, he is a proponent of Test-driven development, Continuous Integration, and Continuous Deployment. He shares his knowledge by speaking at local user groups and conferences, blogging, and writing articles. He is an awarded Microsoft MVP for .NET since 2012.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK