Since the beginning of C#, all reference types in C# are nullable. This means, if you create a variable of any reference type, you can assign a null
to it. If that variable is NULL
, then C# allowed dereferencing it without any checks.
And then, Nullable Reference Types feature was introduced in C# 8. With .NET 6 release, this feature became by default enabled for new project. In this article, we are going to see what this feature is about. We will also see how this feature can be enabled for existing codebases.
The Background
As I was saying earlier, we can assign NULL
to reference type variables. And we can easily dereference such variable. As we all know, dereferencing a NULL
variable would result in a NullReferenceException
.
Sometimes the null assignment may occur in the same method / or inside the same class. Sometimes this null assignment may occur because of the returned value from the called method or API.
The Billion-Dollar Mistake
We all would agree that we may have faced NullReferenceException
a lot of times in many different projects. Sometimes the error might have occurred when you are developing something. In that case, the cost of fixing is very minimal.
Sometimes you may have faced this error, when the code is tested and released to production. In those cases, the cost of fixing this error would certainly have been considerable. Also, if we consider the frequency of these errors, the cost would certainly have multiplied.
Tony Hoare is the man who invented the NULL References. In a 2009 conference, he stated that he invented the null reference in 1965 and it has described this invention as a billion-dollar mistake.
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Tony Hoare
Is null really problematic ?
After reading till this point, a thought may develop suggesting NULLs are really a bad thing. But the problem is – is it really so ?
Let’s say we declare a Student
class, with some properties in it. When we declare a variable of type Student
, what should be the value assigned to it, until we assign the actual instance of Student
?
Also, if we declare array of Student
, what value should be assigned to that variable until that array is populated with actual instances of Student
type ?
I think this explains why NULL is sometimes useful. We may want to assign null to some variables sometimes, to denote that the variable does not have any value. This may be useful in some business flows, where sometimes, some inputs are optional OR when some optional inputs become mandatory if some other option inputs were not provided.
So, now, setting a variable to NULL is not the real problem. The real problem is how are we going to identify whether NULL assignment was really intended or we accidentally received (or set) NULL for a property which is not supposed to be NULL.
Traditional Approach For NULLs
The information provided here is probably not new to most of us. We already knew most of it. Traditionally, we may have been using static analysis rules like this FxCop rule. The static analysis rules suggested to explicitly check the incoming parameters. So we were asked to add null checks in the code , somewhat similar to as shown in the code snippet shown below:
This analysis was limited. It was basically not checking if method accesses any of the global fields or properties. Also, as far as I remember, if input parameter is instance of a class, these static rules were unable to check and suggests if the properties of that instance need null checks. If we dereference the variable which is NULL, it results in NullReferenceException
.
With this code too, the main problem is – for reference types, there is no way to convey the intent which can express if it is good idea to assign NULL to a particular field / property / variable.
What are Nullable Reference Types ?
With C# 8, a new feature – Nullable Reference Types – was introduced. Nullable reference types is basically group of features which were introduced with C# 8 and which can help you to reduce the probability of NullReferenceException
when the application is running.
There are three important features which are part of Nullable Reference Types:
- Improved static flow analysis to decide if a variable may be NULL, before dereferencing it.
- Attributes that annotate APIs that will help flow analysis to decide null-state
- Variable annotation that can help us in explicitly declaring the intended null-state of a variable.
Now, what do these three things mean ?
Static flow analysis will be performed when we try to compile the code, which may produce the warnings. Like other warnings, these warnings can also be ignored, if you really want to ignore them. But it is not recommended.
Those warning convey that the variables may be NULL and hence the code may throw NullReferenceException
at the places where the warnings are raised. As a developer, we should fix these warnings to avoid the runtime exceptions.
While fixing the warnings we can use the attributes / annotations provided by these features to convey our intent to the compiler.
Also, these warnings would appear only if this feature is enabled. Instead of warnings, you can also instruct the compiler to raise the errors if compiler finds any issues during static flow analysis.
This feature was introduced with C# 8,. But prior to .NET 6, this feature was disabled, by default. From .NET 6, all the new projects will have this feature enabled.
How can it be enabled ?
The nullable reference types can be enabled by setting the Nullable element to value “enable
” in the CSPROJ file. In fact, this element in the CSPROJ file can be set to 4 different values – disable, enable, warnings and annotations.
When this element is set to disable, the code behaves as it was behaving when null reference types was disabled. So, if you create a new project in .NET 6 and you do not want this feature enabled (which I am not recommending), then you can set this element to disable.
The value enable means all the nullable reference types features would be enabled. All reference types are non-nullable by default. Refer this documentation for understanding the difference between enable , warnings and annotations.
The above setting enables the feature on project level. So when this feature is enabled, all the code in that particular project will start emitting the warnings if compiler observes any issues during compilation.
Alternatively, there is another way to enable the nullable reference types. We can use compiler directive #nullable to enable (or disable) this feature. This directive can be placed anywhere in the source code.
#nullable enable
: to enable the feature in the following code#nullable disable
: to disable the feature in the following code#nullable restore
: Restores the nullable annotation context and nullable warning context to the project settings.
There are other values too and you can refer the documentation for further details.
Nullable Warnings As Errors
If nullable reference types feature is enabled, the compiler emits the warnings wherever it sees possibility of NullReferenceException
.
If you are working on a codebase which is started with this feature enabled, then you may want compiler to emit the errors instead of warnings. This is for simple reason – warnings can be ignored, but errors cannot be.
In order to instruct compiler to emit errors, we can set the WarningsAsErrors
element in the project file (CSPROJ file) to Nullable
as shown in snippet given below.
If you have an existing code base, then you can enable this feature step by step (project by project). After enabling this feature in a project, you can set this nullable warnings as errors flag in that CSPROJ file.
Note that this setting is not by default added. By default, C# compiler emits warnings. Even if you create a project in .NET 6, it will not have nullable warnings as errors setting. It needs to be enabled explicitly.
Important Operators
We already have been using If statements, null coalescing operator and null conditional operators for null checks in the code.
If nullable reference types feature is enabled, the reference types are non-nullable. In order to declare the nullable reference type, we need to use question mark operator as shown in the code snippet given below. As you can notice the question mark is applied after the type name.
Also, when nullable reference types feature is enabled, compiler performs some checks on the code and emits the warnings. A warning may suggest that a particular reference type may be NULL. In that case, null forgiving operator can be used to convey compiler that this expression is not going to be NULL. This is kind of overruling the judgement provided by compiler.
The null forgiving operator is a postfix operator and it is applied after the variable name.
You can create a console application in .NET 6. It will by default have this feature enabled. Then you can try declaring string variable and try to set it to null (either directly or via condition). You will get warning as shown in the below snapshot – stating that string type is non-nullable and hence null cannot be assigned to it.
Interesting fact – you can declare a non-null reference type and assign null to it if you use the null-forgiving operator after the keyword null.
Wrapping Up
This article was just an introduction to the new feature from .NET 6. There will be coming articles, where we will discuss more about the annotations on API which affect the null-state analysis. But I am sure, this article would have provided you enough information to understand what this feature is and how it can be enabled.
An important point to note is, it seems this feature does not really change anything in the .NET runtime. This feature provides an additional capability at compile time so that runtime exceptions can be reduced.
Nullable reference types aren’t new class types, but rather annotations on existing reference types. The compiler uses those annotations to help you find potential null reference errors in your code. There’s no runtime difference between a non-nullable reference type and a nullable reference type. (from documentation)
Do you want to know any other specific feature from .NET 6 ? Let me know via comments.