My last post was an intro to Any and All – with this post I will weave Where into the mix. I’ll warn that some of this may seem personal preference, but this post is really about what constitutes effective and idiomatic LINQ. IMHO, without having some of these ideas about good LINQ and bad LINQ is a bit like a craftsman that doesn’t care about using a chisel when a screwdriver is appropriate.
Filtered input feeding a LINQ operator
I wish I had a nickel for every time I’ve seen:
// ugh! don't do this: items.Where(x => pred(x)).Any())
I like to think of LINQ as a pipeline that includes a Where like this as “filtered input feeding a LINQ operator.” If the no-argument LINQ operator fed by the Where also has an an overload with a single-argument predicate, then you can always use that rather than the more complex Where…Op, like so:
// do this! items.Any(x => pred(x))
Besides being less code, it’s closer to the predicate-logic (there exists an x such that predicate) kind of language, it’s more declarative and it has a shorter pipeline chain. I think there are plenty of reasons to claim this is idiomatic & effective.
The operator doesn’t have to be just Any that is fed by Where – all of the LINQ operators that have predicate and no-predicate overloads work here: First, Last, Single, Count and of course Any.
|Don’t do this:||Do this:|
…and of course their “OrDefault” overloads where they exist.
Where Feeding a LINQ Operator That Has A Predicate
Similar to Where feeding a non-predicated operator, Where feeding a predicated operator is another case where I generally like to get rid of the Where and roll its predicate into the other operator – we’ll cover rolling Where into All separately because All is special.
Illustrating with Where…Any:
items.Where(x => p1(x)).Any(x => p2(x))
Rolling Where…Any into just Any:
items.Any(x => p1(x) && p2(x))
..but of course it works for First, Last, Count and Single. It doesn’t work for SkipWhile or TakeWhile because they will end up yielding values from the filtered sequence. Since Any and All are related, it may come as a surprise that in regard to combining Where with All, that…
All is special!
But why is All special? Consider this:
items.Where(i => i != null).All(x => x.IsFoo)
What happens if all of the items are null? The above expression will be vacuously true. However, if we combined it like this:
// NOT EQUIVALENT! Don't do this! items.All(x => x != null && x.IsFoo)
If you have an inkling about what the right thing to do is, hold off just a second. We will derive an equivalent expression using the relationship between Any and All from the previous post and the above to arrive at an elegant solution that is correct for all predicates. We can do this by adding a not to the entire expression, changing All to Any and negating All’s predicate, like so:
General form of Where feeding All
items.Where(x => p1(x)).All(x => p2(x))
Let’s first convert this to an equivalent expression with Any instead of All :
!items.Where(x => p1(x)).Any(x => !p2(x))
From the previous section of this post, we know how to combine Where and Any; we delete Where and modify Any‘s predicate, combining the original Where predicate with Any‘s predicate by ANDing them like so:
!items.Any(x => p1(x) && !p2(x))
Now we’re ready to convert Any back to All – we do so exactly the same way we converted All to Any in our first step – we’ll also apply De Morgan’s Laws for negating Any’s predicate. In applying that we end up with the replacement rule we sat out to write (and now we know why All is special):
// yay! We have derived our replacement rule for Where..All items.All(x => !p1(x) || p2(x))
So let’s go back to example that threw things off:
// the original items.Where(x => x != null).All(x => x.IsFoo) // transform for Where..All items.All(x => x == null || x.IsFoo)
If items is empty – this is vacuously true. If items contains only nulls, this returns true. In fact, nulls won’t cause this construct to return false – it’s the nonnull values – if any of them is not an IsFoo, then this construct will return false. Woot! It checks out!
With the exception of the positional predicates that Where supports, Where can always be rolled into Single, First, Last, Any, All, Count and the OrDefault overloads for first three operators (no love for TakeWhile and SkipWhile, they operate on and provide items from the filtered output of Where.) While I’m not dogmatic about this, it’s good to have this under your belt if you want to write the simplest LINQ possible.