Exceptions of type NullReferenceException are the most common errors with .NET applications. With C# 8, nullable reference types have been introduced to get rid of these exceptions. The new .NET 6 and C# 10 project templates turn on nullable reference types. Over the times, many features in regard to nullability have been added to C#! This article shows many of these features.
Nullable Value Types
A value type cannot be null which made it difficult to map it with technologies that allow for optional values, such as the database or XML (nowadays JSON). One way to resolve this is by creating a reference type, and wrapping the value type within – as reference types can be null. C# 2 had another solution: nullable value types. The type
Nullable itself is a value type containing the type T, as well as a
bool value. The Boolean value is used with the
HasValue property, which returns true if the value is not null. Operators are overloaded to allow assigning
The generic type
Nullable specifies the generic type
T with the constraint
struct, thus nullable value types can be used with structs.
Nullable is a struct, the nullable value type does not have the overhead of a reference type – it can be put onto the stack, and the garbage collector is not responsible for the cleanup. The only additional overhead compared to a value type is the
Because this feature is often needed, C# 2 introduced syntax for value types. Instead of using
Nullable x = null, you can use
int? x = null.
More information on nullable value type in Chapter 2, "Core C#" of my book Professional C# and .NET – 2021 Edition.
With C# 6, the null-conditional operators
? have been introduced. Only invoke the right expression if the left expression is not null. The next figure shows calling the
Invoke method of an event only if the event is not null. In the comments above this statement you can see the code that was necessary before this operator was available: first check if the handler is not null before calling the
Using the null-conditional operator, you can also assign the result to a variable – the null-conditional operator returns
null if the expression left of this operator is
? operator can be used to access arrays – null is returned if the array is null.
Null-Coalescing and Null-Coalescing Assignment Operators
The following code snippet shows the null-coalescing operator ??. With
b = a ?? 10,
b gets the value of
a assigned. Just if
a has a value of
null, then the value
10 is assigned to the variable
b. The property shown in the code snippet is of a reference type
MyClass. The variable
_val is declared to be nullable. With the
get accessor of the
Val property, the value of the variable
_val is returned. If
_val has a null value, a new object is created, assigned to the variable
_val, and finally returned from this property.
With C# 8, the null coalescing assignment operator was introduced. This is a combination of multiple operators, such as the
+= addition assigment operator combines the addition and the assignment,
??= combines the null coalescing operator with the assginment. The code shown earlier can be simplified as shown in the next figure. If the variable _val has a null value, a new object is created, assigned to the variable, and returned from the property.
More information on C# operators including the null-conditional, null-coalescing and null-coalescing assingment operators in Chapter 5, "Operators and Casts" of my book Professional C# and .NET – 2021 Edition.
Nullable Reference Types (C# 8)
Nullable reference types have been introduced with C# 8. Enabling this feature, a reference type cannot have null assigned – unless the type is declared with a
?. Because this feature is a breaking change with existing code, it needs to be enabled. Starting with .NET 6 and C# 10, new project templates turn by adding the “ element with the project file.
Enabling this feature adds annotations to method parameters and return values if null is allowed or not. With this, the compiler complains if
null is passed to parameters, and if a possible
null value is returned and used without checking. This helps to avoid exceptions of type
NullReferenceException. This feature doesn’t help to get rid of all the possible nullability issues, but helps getting rid of many of them.
The following feature shows the method
ReturnANullableString returning a nullable string that’s passed into the variable
s. Calling the
ToUpper method of the
string type can only be done before checking if this variable is not null – which is done first with checking for
not null, and then using the null coalescing operator.
Enabling this feature can be done with the
enable configuration in the project file, or using the
#nullable preprocessor directive to set a nullable context for a specific code block. With this preprocessor directive, you can
disable warnings no matter how the project file is configured, and
restore the configuration from the proejct file.
Migrating code to nullability, often some more changes than just adding the annotions for nullability are required. If there’s an issue that cannot be solved that easily, and you know the variable is never null, you can use the null forgiving operator
!. However, usually some light code changes help.
The compiler only accepts initialization of non-nullable reference types within the constructor. If a helper method called from the constructor, or a constructor from a base class does some initialization, you’ll get a compiler warning. One example how such issues can be solved is shown in the following code snippet creating an EF Core context.
Migrating the source code to enable nullable reference types introduced some issues which have been resolved with newer C# and .NET versions. For example, creating generics you had to decide for a generic type T to be either a nullable value or a nullable reference type. Later on other constraints have been added. With the constraint
class, the generic type needs to be a reference type. The constraint
class? allows the type to be a nullable or a non-nullable reference type. The constraint
notnull, the type can either be a struct or a class, but not nullable.
Some attributes have been added for the static analysis for the compiler. One example is
NotNullIfNotNull which specifies that the return value is not null if the argument where the attribute is used is not null. The following code snippet shows the user of
NotNullWhen with a
TryParse method: when the return type is true, the out value
IPEndPoint returned is not null. If the method returns false, IPEndPoint can be null. See a link for all the attributes available for null-state static analysis with a link below.
ArgumentNullException (.NET 6, C# 11)
Even if you use nullable reference types, an exception of type
ArgumentNullException should be thrown if a null value is passed to the method not expecting null. With the nullable annotation, the compiler generates a warning passing the wrong value. You can even change it to an error instead of a warning. However, not probably not all clients (if you create a library) have nullable reference types enabled, and thus it’s a good practice to throw exceptions if null is passed.
The next image shows the traditional way to throw exceptions of type
With .NET 6 you can reduce the number of code lines needed with the new static method
ThrowIfNull. ThrowIfNull has a second parameter that is optional, and by default – using the attribute
CallerArgumentExpression – takes the name of the parameter, similar to the implementation before with the
A new extension with C# 11 is the bang-bang operator
!!. This operator just needs to be added to the parameter, and the implementation to throw an
ArgumentNullException is generated from the compiler:
You might think that the bang-bang operator is not necessary anymore having nullable reference types. Why should you check for
null, if the compiler already complains if
nullis passed as a parameter to a method if
nullis not expected. You can also change the compiler setting to having errors instead of warnings. If you already migrated all your code to nullable reference types, you might get rid of some nullability checks. However, creating libraries you should keep adding checks. As nullable reference types are a breaking change, it will take many years until all the existing code is converted. Having a little
!!in your code increases developer productivity a lot instead of creating a few code lines that need to be written again and again.
Over the times, more and more features for nullability have been added to C# – starting with nullable value types, and the breaking change with nullable reference types. Some features are here to increase the code quality. As a
NullReferenceException is the most common exception with .NET applications, the code quality can be greatly improved. Other features are here to reduce the number of code lines to write, examples are the null-coalescing assignment operator and the bang-bang operator.
Enjoy learning and programming!
It would be great if you show support buying a coffee.
For more information on C# read my new book 🙂
See Chapters 1-13 for the foundations of C#, 14-23 .NET platform features, 24-28 ASP.NET Core, and 29-31 WinUI!