Подтвердить что ты не робот

Почему '.Select(...). Last()' оптимизирован, а ".Select(...). Last (...)" нет?

Рассмотрим следующий перечислитель:

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

Это дает элементы [1, 2, 3, 4, 5], печатая их по мере их использования.

Когда я вызываю метод Last для этого перечислителя, он запускает быстрый путь, который обращается только к одному элементу:

items.Last();
inspect 5

Но когда я передаю обратный вызов Last, он перебирает весь список с самого начала:

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

Просматривая исходный код .NET Core, я обнаружил, что:

С другой стороны:

Это объясняет, как случай обратного вызова не оптимизирован. Но это не объясняет почему.

Концептуально, если хотя бы один элемент удовлетворяет предикату (что вероятно на практике), то итерация в обратном направлении может позволить досрочно выйти из цикла.

Это также не кажется сложным для реализации: из того, что я видел, все, что требуется, это дополнительный метод в IPartition<T>.

Отсутствие оптимизации также может быть удивительным. Поскольку эти перегрузки имеют одно и то же имя, можно предположить, что они также оптимизированы аналогичным образом. (По крайней мере, так я и думал.)

Учитывая эти причины для оптимизации этого случая, почему авторы LINQ решили не делать этого?

4b9b3361

Ответ 1

Last() всегда может быть оптимизирована для коллекций, которые обеспечивают доступ к последнему элементу коллекции в постоянное время (O(1)). Для этих коллекций вместо итерации всей коллекции и возврата последнего элемента вы можете просто получить доступ к последнему элементу напрямую.

Концептуально, если хотя бы один элемент удовлетворяет предикату (что вероятно на практике), то итерация в обратном направлении может позволить досрочно выйти из цикла.

Это предположение слишком далеко для общей реализации Last(Func<T,bool>). Вы не можете предположить, что последний элемент, который удовлетворяет предикату, ближе к концу коллекции в целом. Эта оптимизация хорошо работает для вашего примера (Last(x=>true)), но для каждого такого примера может быть противоположный пример, где эта оптимизация работает хуже (Last(x=>false)).