Skip to content

Instantly share code, notes, and snippets.

@BenMakesGames
Last active December 16, 2022 18:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BenMakesGames/80630d1fd8f738a83ef07b05f624a29c to your computer and use it in GitHub Desktop.
Save BenMakesGames/80630d1fd8f738a83ef07b05f624a29c to your computer and use it in GitHub Desktop.

Having fun (or not) with LINQ and nulls

How to Where and Select your nulls away

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 nulls in there; sometimes there's customers... and I want to get a List<long> of all of their Ids; 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 nulls, 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 nulls 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 nulls, so we're safe.

Take-aways

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 nulls, 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 nulls 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.

Further Reading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment