Navigating Non-Nullable Reference Types in C#: Enhance Code Safety
I hate NullReferenceException
But one thing I hate the most is checking variables when they are not supposed to be empty or null, especially in these two cases:
myString != null
when the string argument is not specified as NullablemyEnumerable != null
when it is not supposed to be (especially for extension methods)
Fortunately, this should be over soon.
C# 8.0 has introduced that reference types are not null by default. What does this mean?
Types that are not specified as Nullable can no longer be equal to null, the same way nullable references are already handled for native classes (bool, int, float and so on).
int myInt1 = 2 // OK
int? myInt2 = 2 // OK
myInt1 = null // throw Exception
myInt2 = null // OK
How does it works on the code side?
public class Goat
{
public string Color { get; set; }
public string AwesomeName { get; set; }
}
public class MyTest
{
public function MyGoatTest
{
var goat = new Goat();
var goatName = goat.AwesomeName;
// Here, goatName == null
}
}
A Goat has 2 properties: Color
and AwesomeName
.
This leads to checking both properties each time to make sure neither is null, even if the declaring type is string
and not string?
.
So what is the point of making these properties non-null if they are always null at initialization?
Use ? to declare nullable
If both properties can be null, the best solution is to declare the type as nullable by adding the question mark ?
.
public class Goat
{
public string? Color { get; set; }
public string? AwesomeName { get; set; }
}
It seems strange to add a question mark to a string
type, but why not? We already do it for many types like bool
, int
and so on. Why not do it for all types?
In this way, the Goat object admits that the Color
and AwesomeName
properties can be null, and it makes sense to check with myString != null
each time they are used to be sure they contain a value.
Forcing not nullable values
This is the easy part now!
Properties can no longer be equal to null, which means that... They have to be initialized!
The best known method is to use constructors to ensure that each property is defined.
public class Goat
{
public Goat(string color, string awesomeName, Grassland grassland, string[] skills)
{
Color = color;
AwesomeName = awesomeName;
Grassland = grassland;
Skills = skills;
}
public string Color { get; set; }
public string AwesomeName { get; set; }
public string[] Skills { get; set; }
public Grassland Grassland { get; set; }
}
// Here is the beautiful Grassland class, awesome right?
public class Grassland
{
public int Surface { get; set; } = 0;
}
But constructors imply that you have all the data you need to build your object.
This is not always true. The fact is that a non-nullable property does not mean that it has to be set, it can also mean that it contains a default value.
For that, don't worry, we already have everything we need:
- Strings with
string.Empty
- Types with constructors (do not use
default
as initializer since it's equals to null for objects) - Enumerable from with
new List<T>
/T[]
as needed.
public class Goat
{
public string Color { get; set; } = string.Empty;
public string AwesomeName { get; set; } = string.Empty;
public string[] Skills { get; set; } = Array.Empty<string>();
public Grassland Grassland { get; set; } = new Grassland();
}
You can know be sure that each properties is different from null at 100%.
Handling methods return type
The point of keeping in mind the difference between T?
and T
is to avoid unnecessary conditions to check an element.
public string GetGoatFullName(Goat goat) {
// Do not have to check if properties are null since it's declared at string not nullable
return $"{goat.AwesomeName} {goat.Color}";
}
public void TestClass() {
var myGoat = new Goat();
var fullName = GetGoatFullName(goat);
// Do not have to check if fullname is null
if(fullName == "Chicky Orange") {
Console.Log("What an awesome name");
}
}
GetGoatFullName()
returns string
, so the fullName != null
condition will check for something that can no longer happen.
It may be worth checking for emptiness if, and only if, you need to check it. In this case, we don't mind if fullName is empty!
The code is, in my opinion, cleaner and more readable!
How does it works on the compilator side ?
This handling of Non-Nullable and Nullable generates attributes on compilator:
[DisallowNull]
and[AllowNull]
to specify preconditions on properties and methods arguments[NotNull]
and[MaybeNull]
to specify postconditions on methods return values
To go further:
Null-forgiving operator
You may have seen one of these warnings:
- Warning CS8602: Dereferencing a possibly null reference
- Warning CS8625: Cannot convert null literal or possible null value to non-nullable type
Even if you have specified your T
object as non-nullable, nothing (for the moment) prevents you from assigning the null value.
You mainly get these cases when you forget to initialize your properties.
As the opposite of the ?
operator which converts a Non-Nullable to Nullable, the !
operator converts a Nullable to Non-Nullable.
This means that even though the result may be null, it is considered Non-Nullable.
The null-forgiving operator has no effect at run time. It only affects the compiler's static flow analysis by changing the null state of the expression. At run time, expressionx!
evaluates to the result of the underlying expressionx
.
Here is a great link to learn more about this :
Warning: I do not recommend using this, but it's worth it to understand how it works!
I hope you've learned something. If you did, don't forget to subscribe to be aware of all news articles in coming!