Nulls have always presented programming challenges.
To define a null, I'll quote MS-Docs:
The null keyword is a literal that represents a null reference, one that does not refer to any object. null is the default value of reference-type variables. Ordinary value types cannot be null, except for nullable value types.
C# 8 introduced nullable reference types. A system where reference types could no longer be null unless explicitly declared with the ?
operator.
// not nullable fields/properties must be assigned a value string notNullableValue = string.Empty; string? NullableValue; // nullable is controlled by the return value from the method var value = SomeMethod();
For many programmers this had no consequences because the project templates by default did not enable Nullable. We sailed on serenely in ignorance.
With Nullable enabled the compiler throws warnings whenever it considers you've broken the rules. It's pretty good, but there are certain circumstances where you want to break the rules, and edge cases where it gets things wrong. We'll look at both of those later.
Visual Studio 2022 moved the goalposts: Nullable
is now enabled by default. Here's the project file for a console app:
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup>
You now have to explicitly disable it. Write code the old way, or import existing code and unless you were a miracle coder, you will see a lot of null code warnings and errors. It has certainly impacted my coding style and practices.
Let's look at some code to see what I mean.
Here's some simple old style code.
string value; value = getHello(0); Console.WriteLine(value); string getHello(int test) => test == 1 ? "Hello World" : null;
Disable Nullable and this code flies: no exceptions or warnings. So what's the problem?
String can be a null, so we can pass null strings around. Unless we explicitly check, we don't know if Console.WriteLine
handles nulls correctly. If it doesn't we will get an exception when we pass it a null. Everything flies because Console.WriteLine
knows how to handle nulls. It's definition looks like this:
Console.WriteLine(string? value)
Enable Nullable and we see a warning - Possible Null Reference Return on getHello
. Why?
getHello
declares it's return value as string
. No ?
and therefore not nullable, yet in the body we can return null
.string value
also declares value
as not nullable.To fix this:
string? value; value = getHello(0); Console.WriteLine(value); string? getHello(int test) => test == 1 ? "Hello World" : null;
We've added the nullable ?
operator to value
and the return declaration of getHello
.
We're now declaring and handling nullables correctly and the compiler is happy,
Our new block of code
string? value; var sometest = true; value = sometest ? "Hello World" : "Bye World"; WriteLine(value); value = sometest ? "Hello World" : null; // Get a possible null warning WriteLine(value); void WriteLine(string value) => Console.WriteLine(value);
We have our own WriteLine
method with a not nullable string argument. note that we only get a compiler warning after yhe second assignment to value where we try and assign a null
. The compiler is analysing the code, seeing a possble null and throwing a warning.
We fix this by changing WriteLne
to accept a nullable string. The compiler knows Console.WriteLine
accepts nullable strings so everything is good.
void WriteLine(string? value) => Console.WriteLine(value);
The contrived code below illustrates one common problem. We know once we've called GetAsync
and assigned value
it can't be null because we throw an error if it is. However, the analyser isn't that clever so still throws the warning.
string? value = null; var myClass = new MyClass(); await myClass.GetAsync(); value = myClass.Value; // We know it's safe to pass Value because it can't be null // but the code doesn't so still throws a warning WriteLine(value); void WriteLine(string value) => Console.WriteLine(value); class MyClass { public string? Value { get; set; } public async Task GetAsync() { await Task.Yield(); // Get the value from a service this.Value = "Hello World"; if (this.Value is null) throw new ArgumentNullException("Value is null and shouldn't be!"); } }
We can suppress these messages using the ! null forgiving operator. In the code above, the correct place to apply it is where the assignment takes place.
value = myClass.Value!;
Don't apply it to where it's being used.
WriteLine(value!);
Two notes:
Don't use it unless you have to. Use null coalesing - covered below - wherever you can.
The null forgiving operator has no effect in compilation. It's just a succinct message from the programmer to the compiler/interpreter saying "No worries mate, bin the warnings, I've got it covered!".
The example above may be contrived, but consider the following code from a Blazor page.
[Inject] MyService? myService { get; set; } void DoSomething() { // nullable warning on myService var value = myService.Value; }
Blazor ensures MyService
is registered: it throws an exception if it isn't. We could code:
[Inject] MyService myService { get; set; } = new MyService();
Which works, but is just wrong. We're creating an object for a contrived reason, and it's absolutely useless created outside the DI container context.
We could use null forgiving when we use the object, which is ok when you use ut once, but say you use the service ten times in you code block, it doesn't seem right.
My current best solution is to use a local variable in a code block like this:
void DoSomething() { MyService service = myService!; var value = service.Value; .... }
Not often, but testing is one area where you may break the rules to test null handling and boundary conditions.
The normal approach to the "boxing" problem of switching from nullable to non nullable is to use null coalescing ??
.
We can use it on Writeline
like this. Writeline
now receives value
if it isn't null
, or string,Empty
if it is.
WriteLine(value ?? string.Empty);
We can also use the very useful null coalescing assignment operator ??=
like this whenever we need to make sure a value isn't null:
value ??= string.Empty;
WriteLine(value);
More succinct than:
if (value is null) { value = string.Empty; }