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? => stringIf 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 enableAs 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 annotationsTo 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 nullis 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.