Why is `.Select(...).Last()` optimized, but `.Select(...).Last(...)` not?

asked7 years ago
last updated7 years ago
viewed337 times
Up Vote15Down Vote

Consider the following enumerator:

var items = (new int[] { 1, 2, 3, 4, 5 }).Select(x =>
{
    Console.WriteLine($"inspect {x}");
    return x;
});

This yields the elements [1, 2, 3, 4, 5], printing them as they are consumed.

When I call the Last method on this enumerator, it triggers a fast path which only accesses a single element:

items.Last();
inspect 5

But when I pass a callback to Last, it loops through the whole list from the beginning:

items.Last(x => true);
inspect 1
inspect 2
inspect 3
inspect 4
inspect 5

Looking through the .NET Core source code, I find that:

On the other hand:

This explains the callback case is not optimized. But it doesn't explain .

Conceptually, if at least one element satisfies the predicate (which is likely in practice), then iterating backward may allow for exiting the loop early.

It doesn't seem difficult to implement either: from what I've seen, all it takes is an additional method on IPartition<T>.

The lack of optimization can also be surprising. Since these overloads share the same name, one might assume that they are also optimized in a similar way. (At least that's what I thought.)

Given these reasons to optimize this case, why did the authors of LINQ choose not to do that?