All examples from this article are available on GitHub:
https://github.com/BigLittleEndian/NullableInCSharp
After switching to .NET 6 and getting some warnings related to dereferencing nullable types I got an idea to write a complete article about nullable in C#. If you are someone who stopped learning about nullable after C#2 and introduction of nullable value types, this is the right article for you. It will show you all features related to nullable types and references up to and including C#10.
History
null
is a concept existing in many programming languages. Value null
indicates that the pointer or reference does not point
or refer to a valid object. It was introduced in 1965 as a part of the ALGOL W
language. Its creator Tony Hoare (also famous for his Quicksort
algorithm) later coined the term for it as a “billion dollar mistake”.
Nullable Value Types in C#2
If you create an integer (value type) and never assign to it, it will have a default value of 0. Problem is that 0 could be a legitimate value representing something other than “never assigned” or “undefined”. So, people started to invent values that represent “undefined”. I call those values “magic values” since every organization, every person, every project has different representation of “undefined”. Therefore, introducing nullable value types in C# 2.0 was a step in the right direction. The code below is showing how the magic number could be replaced with nullable value type.
Suffix “?” after value type is making it nullable. Its
default value is null
.
Any nullable value type is an instance of the generic System.Nullable<T>
structure.
Appending “?” to int
will create System.Nullable<int>
structure.
This structure is also adding some utilities like HasValue
helper property from example above.
When working with nullable types you always need to check for null before checking the value:
There is a shorter way to check for null
and value:
Nullable Reference Types in C#8
Code Is Not Changing
Feature name is confusing since reference types are already nullable. What is introduced
in C#8 is a way to express intent and tell compiler that some objects could be null
,
and some objects will never be null
. Using that information compiler is doing static
analysis at design-time to determine the null-state of a variable and with warnings helping
us to avoid NullReferenceException
at the runtime.
string
) will not create some new type or structure as it does for value types.
(value type) int? => System.Nullable<int>
(reference type) string? => string
If string is not changed after appending “?” what is actually added in C#8?
C# 8 introduced two new fine-grained controls for how the compiler interprets reference type variables:
- Annotations - if enabled you can use suffix “?” and null forgiving operator “!” to express intention so compiler can generate warnings.
- Warnings - if enabled the compiler performs all null analysis and emits warnings when code might dereference null.
Nullable reference types are compile time check. You express intent, compiler uses it to produce warnings. That is all.
New Bits from Nov 2021
.NET 6 project templates are turning on those nullable reference types features by default. Also, C#10 has number of improvements to null state analysis. Therefore, you will get fewer false positive nullable warnings.
Did You Turn Them on and off?
You can turn features on/off both or separately. Also, you can specify features per project or per file. If you are enabling nullable references feature on a big project, you can do it in chunks by enabling it on a single file:
#nullable enable
As mentioned above you can turn only one feature. As per example below annotations will allow use of suffix “?” but without warnings:
#nullable disable warnings
#nullable enable annotations
To be honest I don’t see much practical use of tuning on only one feature.
How It Works
Let’s have an example showing warnings already in the constructor (all examples in this article assume that nullable reference feature is enabled):
All 3 string members are the same and if we don’t assign any value to them, they will be null
.
MiddleName
has suffix “?”. That is not changing underlying variable. With “?” we only
instructed compiler that MiddleName
can be null
at any point of time.
Default state of a non-nullable reference variable is not-null so First and Last should never be null
.
Compiler gets this seriously and starts complaining right away if we don’t assign
FirstName
and LastName
in the constructor.
Compiler is OK if we don’t assign MiddleName
or if we assign null to it.
var
) are nullable reference types.
Observing the next piece of code will explain why using nullable reference types is a good idea.
Compiler is showing warning only for MiddleName: Dereference of a possibly null reference
.
And it is 100% correct. FirstName
and LastName
name are never null
,
and MiddleName.Length
will throw exception if MiddleName
is null
.
To be more precise, FirstName
and LastName
still can be null
if they
are set to null
in some other piece of code. A warning will be issued for that piece of code,
not here.
Correct code is below. Before using it, we need to check MiddleName
for null
.
There is a simpler way to guard from dereferencing null
by using null-coalescing operator ??
or
null-conditional operator ?.
. For the sake of learning we will use if
statement.
Compiler did static analysis and determined that we will call MiddleName.Length
only when
MiddleName
is not null
, so no warning is fired.
is not null
is null
==
or =!
.
Note that string?
doesn’t have HasValue
property like int?
.
Reminder, nullable reference types are not changing IL.
You Can Still Write Bad Code
Even with nullable warnings turned on you can write bad code where compiler will not generate warning,
but runtime will crash with null exception. Especially if you are working in multithreaded environment there is
no guarantee that expression evaluated by if
will evaluate same not-null
value again
within if
block. Here is a simplified example:
Without guard if(MiddleCount.HasValue)
compiler is generating a warning. Once we put guard compiler is happy.
It is not smart enough to understand that inside if
block MiddleCount
will become null
once we reset MiddleName
. Without any warnings code will crush in runtime for anyone with middle name.
Pitfalls
Current null state analysis is not covering all possible problems. First example is array of strings (see below).
No warning is fired and code will crash at runtime on array[5].Length
. Second example is related to default
structure. For member Name
we are not getting warning like we did for student’s first and last name in
previous example. Student
was a class; Person
is a structure. So, if we pass default structure
to PrintPerson()
, Name
is null
and without any warnings code will crash at runtime.
! (Null-Forgiving) Operator
Sometimes you want intentionally to pass or assign null
, and you don’t want compiler to complain about it.
Most probable use case for this is unit testing.
Let’s assume we want to test this method:
Our test wants to make sure that illegal parameter null
will throw exception, but we don’t want to get warning.
Passing null
value or nullable variable with suffix “!” will make compiler trust you know what you are doing,
and it will not fire warning for calling SetFirstName()
with null
:
This null-forgiving operator could be applied to literal or variable: null!
or someVariable!
.
You can use this operator like: MiddleName!.Length
in case compiler is complaining and you are 100% sure
MiddleName
is not null
.
Attributes
You can use precondition attributes [AllowNull]
and [DisallowNull]
instead of appending “?” or
leaving it out. The only difference is that you will not get a proper help from IntelliSense.
More useful are postcondition attributes. Those are like hints for null state analysis helping it to understand the
semantics of your other methods (API or helpers). .NET guys already applied attribute to string.IsNullOrEmpty()
so you can use it to guard dereferencing nullable strings. Examples will make it more clear.
Postcondition attributes are MaybeNull
and NotNull
. In helper method below we are using
attribute to tell compiler that string passed as a parameter will not be null
once method returns.
Again, code could be shorter but for the sake of learning we use if
statement.
Using this method in constructor below we are avoiding compiler warnings. Once you remove attribute from helper method warnings will appear in the constructor.
Conditional postcondition attributes are MaybeNullWhen
, NotNullWhen
and NotNullIfNotNull
.
In example below, string passed as an argument is not null
if function returns true.
Code below is without warnings since compiler static analysis uses attribute to understand that within if
block
MiddleName
is never null
.
Method and property helper methods attributes are MemberNotNull
and MemberNotNullWhen
.
Best usage for this is making a constructor helper method to set some properties outside the constructor:
With MemberNotNull
attribute we are telling caller (constructor) that this method is setting
FirstName
and LastName
to not null
so no warnings will be fired.
Unreachable code attributes are DoesNotReturn
and DoesNotReturnIf
. In example below, helper
method is letting the compiler know that if argument is true, code will throw exception, therefore all code after calling
this method will be unreachable.
In method below compiler did analysis and concluded that if MiddleName
is null
, FailIf()
method will throw and MiddleName
will never be dereferenced, so there is no reason to warn.
Generics
For generics we need to specify capabilities and expectations of a type parameter. With those we can also express nullability:
-
where T : class
- Reference type. T must be a non-nullable reference type. -
where T : class?
- Reference type, either nullable or non-nullable. -
where T : notnull
- Non-nullable type. The argument can be a non-nullable reference type or a non-nullable value type.
I didn’t list <base class>
& <interface name>
with and without ? since the idea is the same.
Example below is showing notnull
constraint. T
can be value or reference type but as we will see
in the usage later, they are not working the same. What is important to notice is that in Text
getter only
NullableValue
must be guarded and checked for null. Value
is of type T
and constraint
is making is sure that T
is not nullable.
There is a different behaviour for value type and reference type (see example below). When T
is string
,
constructor has parameters string
and string?
. When we make same generic from int
,
constructor is expecting int
and int
. Second parameter is not nullable! Not sure why but at least
this is how it works in C#10 for now.
In the last example we are passing nullable string and that is against notnull
constraint. Compiler will fire a warning.
If we ignore this warning and pass null
as a first parameter, Text
getter will crash on not guarded
Value.ToString()
.
You can play with class?
constraint and you will notice that if T
is string?
,
T?
is same string?
(second question mark is ignored). This relaxation of the rule is introduced in C# 9.0
Conclusion
When I decided to write about nullable my expectation was that it will be one of the shortest articles. At the end I needed to skip some stuff to keep article from being too long. I partially ignored nullable reference types when they appeared in C#8, but .NET 6 and nullability turned on by default triggered me to think more about it. It is a good feature.