As an addition to the series on nullability in C#, we should look at the "var"
keyword. This is because "var" behaves a little differently than it did before
nullable reference types. Specifically, when nullability is enabled, using
"var' with a reference type always results in a nullable reference type.
Articles
- Nullability in C# - What It Is and What It Is Not
- Null Conditional Operators in C# - ?. and ?[]
- Null Forgiving Operator in C# - !
- Null Coalescing Operators in C# - ?? and ??=
- C# "var" with a Reference Type is Always Nullable (this article)
The source code for this article series can be found on GitHub:
https://github.com/jeremybytes/nullability-in-csharp.
The Short Version
Using "var" with a reference type always results in a nullable reference type.
var people = new List<Person> {...}
When we hover over the "people" variable name, the pop-up shows us the type is
nullable:
(local variable) List<Person>? people
So even though the "people" variable is immediately assigned to using a constructor, the compiler
still marks this as nullable.
Let's take a look at how this is a little different than it was before.
Before Nullable Reference Types
Before nullable reference types, we could expect the type of "var" to be the
same as the type of whatever is assigned to the variable.
Consider the following code:
Person person1 = GetPersonById(1);
var person2 = GetPersonById(2);
In this code, both "person1" and "person2" have the same type: "Person"
(assuming that this is the type that comes back from "GetPersonById").
We can verify this by hovering over the variable in Visual Studio. First
"person1":
(local variable) Person person1
This shows us that the type of "person1" is "Person".
Now "person2":
(local variable) Person person2
This shows us that the type of "person2" is also "Person".
The lines of code are equivalent. "var" just acts as a shortcut here. And for
those of us who have been coding with C# for a while, this is what we were
used to.
If you would like a closer look at "var", you can take a look at "Demystifying the 'var' Keyword in C#".
After Nullable Reference Types
But things are a bit different when nullable reference types are enabled.
Here is the same code:
Person person1 = GetPersonById(1);
var person2 = GetPersonById(2);
When we hover over the "person1" variable, Visual Studio tells us that this is
a "Person" type (what we expected.
(local variable) Person person1
When we hover over the "person2" variable, Visual Studio tells us that this is
a "Person?" type -- meaning it is a nullable Person.
(local variable) Person? person2
So this shows us that the type is different when we use "var". In the code
above, "GetPersonById" returns a non-nullable Person. But as we saw in the
first article in the series, that is not something that we can rely on at runtime.
The Same with Constructors
You might think that this behavior only applies when we assign a variable
based on a method or function call, but the behavior is the same when we use a
constructor during assignment.
In the following code, we create a variable called "people" and initialize it
using the constructor for "List<Person>":
var people = new List<Person> {...}
When we hover over the "people" variable name, the pop-up shows us the type is
nullable:
(local variable) List<Person>? people
So even though the "people" variable is assigned based on a constructor call,
the compiler still marks this as nullable.
Confusing Tooling
One reason why this was surprising to me is that I generally use the Visual
Studio tooling a bit differently then I have used it here. And the pop-ups
show different things depending on what we are looking at.
Hover over "var"
Normally when I want to look at a type of a "var" variable, I hover over the
"var" keyword. Here is the result:
class System.Collections.Generic.List<T>
...
T is Person
This tells us that the type of "var" is "List<T>" where "T is Person".
This means "List<Person>". Notice that there is no mention of
nullability here.
Hover over the variable name
However, if we hover over the name of the variable itself, we get the actual
type:
(local variable) List<Person>? people
As we've seen above, this shows that our variable is, in fact, nullable.
The Documentation
There's documentation on this behavior on the Microsoft Learn Site: Declaration Types: Implicitly Typed Variables. This gives some insight into the behavior:
"When var is used with nullable reference types enabled, it always implies a nullable reference type even if the expression type isn't nullable. The compiler's null state analysis protects against dereferencing a potential null value. If the variable is never assigned to an expression that maybe null, the compiler won't emit any warnings. If you assign the variable to an expression that might be null, you must test that it isn't null before dereferencing it to avoid any warnings."
This tells us that the behavior supports the compiler messages about potential null values. Because there is so much existing code that uses var, there was a lot of potential to overwhelm devs with "potential null" messages when nullability is enabled. To alleviate that, "var" was made nullable.
Should I Worry?
The next question is whether we need to worry about this behavior. The answer
is: probably not. In most of our code, we will probably not notice the
difference. Even though the "var" types are technically nullable, they will
most likely not be null (since the variables get an initial assignment).
But it may be something to keep in the back of your mind when working with
"var" and nullable reference types. If you are used to using "var" in a lot of
different situations, you just need to be aware that those variables are now
nullable. I know of at least 1 developer who did run into an issue with this, but I have not heard about widespread concerns.
Wrap Up
It's always "fun" when behavior of code changes from what we are used to. I
did not know that this behavior existed for a long time -- not until after I
saw some high-profile folks talking about it online about 2 months ago. I finally put this article together because I am working on a presentation about nullable reference types that I'll be giving in a couple months.
My use of "var" is not changing due to this. Historically, if the type was
clear based on the assignment (such as a constructor), then I would tend to
use "var". If the type was not clear based on the assignment (such as coming
from a not-too-well-named method call), then I tend to be more explicit it my
types.
Of course, a lot of this changed with "target-typed new" expressions (from C# 9). But I won't go into my thoughts on that right now.
Happy Coding!
I haven't tried this, but would an exclamation help?
ReplyDelete