Wednesday, February 22, 2023

C# "var" with a Reference Type is Always Nullable

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

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!

1 comment: