Suppose I've got this list:
var customers = new List<Customer?>()
{
new Customer() { Id = 123, CompanyName = "Bla" },
null,
new Customer() { Id = 555, CompanyName = "Whatever" }
};
A list of nullable Customer
objects... sometimes there's null
s in there; sometimes there's customers... and I want to get a List<long>
of all of their Id
s; I want to get { 123, 555 }
.
If I do the following:
var ids = customers.Select(c => c.Id).ToList();
C# will say "I can't compile this, because c
can be null
, and I can't do null.Id
- that would be madness!"
(This document assumes we're using nullable reference types, because it's the right thing to do (for just these kinds of reasons). There's some links to more info about nullable reference types in the "further reading" section at the end of this document.)
I can make C# happy by throwing in a question mark:
var ids = customers.Select(c => c?.Id).ToList();
This makes C# happy, because when the code hits the null
customer, and evaluates null?.Id
, C# returns null
(that's what the ?.
operator does), BUT: this doesn't make me happy, because that just gives me a list like this: { 123, null, 555 }
, a List<long?>
, and I'm looking for a List<long>
Attempt #2:
var ids = customers.Where(c => c != null).Select(c => c.Id).ToList();
This seems like it will work, but C# will still complain about the c.Id
, saying "I can't compile this, because c
can be null
, and" bla-bla-bla.
So why isn't this working as expected?
Let's break the problem into pieces:
var filteredCustomers = customers.Where(c => c != null);
In the above code, filteredCustomers
is still of type List<Customer?>
- nullable Customer
objects.
Why?
Because the Where
method is a method like any other, with a return type, and return types can't change based on the method's inputs.
Like, if I write the method string DoStuff(int someNumber)
... well... that's what I wrote! DoStuff
takes an int
, and returns a string
, and that's all it can do. There's not a way to somehow return something different based on what int
was passed in, or anything else. It was written with the return type string
.
Similarly, the Where
method on a list returns the same type as the list; that's how it was written. If we call Where
on a List<string>
, we get a List<string>
; if we call Where
on a List<Customer?>
, it returns a List<Customer?>
; etc. That's how it was written. That's all it can do.
So what can I do? Has my quest for a List<long>
come to a bitter end?!?!
No: there is still hope.
I can super-duper-promise C# that "yo, it's fine; nothing is null
here". Here's one way to do that:
var ids = customers.Where(c => c != null).Select(c => c!.Id).ToList();
Here, c!.Id
, tells C# "I super-duper promise: c
is not null
, so don't worry about it; you can go ahead and grab its Id
".
And this !
tool can be used in many places in C#, to promise "it's not null
"... but we have to use it carefully! If I promise C# that something isn't null
, but I was wrong, then I may get a null reference exception that crashes the program.
In the code I'm writing now, it is very clearly & obviously filtering out null
s, so I can feel pretty confident in myself that I'm not messing up.
Here's another way to convince C# that some nullable things aren't null
:
var ids = customers.Where(c => c is not null).Cast<Customer>().Select(c => c.Id).ToList();
The Cast
method is doing normal C# typecasting under the hood; it's roughly the same as writing this:
var ids = customers.Where(c => c != null).Select(c => (Customer)c!).Select(c => c.Id).ToList();
But notice that the above code still has to use an exclamation mark, to super-duper promise "there's no null
s here". Cast
does this, too, we just don't see it, since it's done inside the Cast
method. So these two solutions aren't any safer than the first one; typecasting to a non-nullable type will crash if given a null
. But again: Where(c => c != null)
is definitely filtering out any null
s, so we're safe.
1. If you can write your code such that there aren't nullable values in the first place, that's great, and you'll have a happier, and less bug-prone time
Nullable values are a little more annoying to work with, since we need to check null
s, and do it right. If we fail to do it right, we introduce the possibility of a null reference exception.
In the code examples in this document, it was very clear & obvious that null
s were being safely handled, but it's not always so obvious: sometimes code is split over multiple lines, or even multiple methods; sometimes you, or another developer, may come along later to change or remove the Where
, and fail to notice the super-duper promises to C# that are elsewhere in the code...
This is not to say "never use nullable values" - sometimes null
is a very meaningful and useful value to store, like when a user hasn't provided any input yet, or when we're searching for something in a list but may not find it (FirstOrDefault
) - we just have to be a little more careful when handling them.
2. If you have to use ?
and !
a lot to work around nullable values, that's a "code smell" - a sign that the code could probably be tidier.
In such cases, see if you can deal with the nullable stuff earlier in the code, to get it out of the way.