Фильтрация циклов foreach по условию where против продолжения guard

24

Я видел, как некоторые программисты используют это:

foreach (var item in items)
{
    if (item.Field != null)
        continue;

    if (item.State != ItemStates.Deleted)
        continue;

    // code
}

вместо того, где я обычно использовал бы:

foreach (var item in items.Where(i => i.Field != null && i.State != ItemStates.Deleted))
{
    // code
}

Я даже видел комбинацию обоих. Мне очень нравится удобочитаемость с «продолжить», особенно в более сложных условиях. Есть ли разница в производительности? С запросом к базе данных я предполагаю, что будет. Как насчет обычных списков?

Paprik
источник
3
Для обычных списков это звучит как микрооптимизация.
Апокалипсис
2
@zgnilec: ... но на самом деле какой из двух вариантов является оптимизированной версией? Конечно, у меня есть мнение по этому поводу, но из-за того, что я просто смотрю на код, это не всем понятно.
Док Браун
2
Конечно, продолжение будет быстрее. Использование linq. Где вы создаете дополнительный итератор.
апокалипсис
1
@zgnilec - Хорошая теория. Не забудьте опубликовать ответ, объясняющий, почему вы так думаете? Оба ответа, которые существуют в настоящее время, говорят об обратном.
Бобсон
2
... Итак, суть в том, что различия в производительности между этими двумя конструкциями пренебрежимы, и для обоих могут быть достигнуты удобочитаемость и возможность отладки. Это просто вопрос вкуса, который вы предпочитаете.
Док Браун

Ответы:

64

Я бы посчитал это подходящим местом для разделения команд и запросов . Например:

// query
var validItems = items.Where(i => i.Field != null && i.State != ItemStates.Deleted);
// command
foreach (var item in validItems) {
    // do stuff
}

Это также позволяет вам дать хорошее самодокументируемое имя для результата запроса. Это также помогает вам увидеть возможности для рефакторинга, потому что намного проще реорганизовать код, который только запрашивает данные или только изменяет данные, чем смешанный код, который пытается сделать оба.

При отладке вы можете сломаться раньше foreach быстро проверить, соответствует ли содержимое validItemsтому, что вы ожидаете. Вам не нужно входить в лямбду, если вам не нужно. Если вам нужно войти в лямбду, тогда я предлагаю выделить ее в отдельную функцию, а затем пройти через это.

Есть ли разница в производительности? Если запрос поддерживается базой данных, то версия LINQ потенциально может выполняться быстрее, поскольку запрос SQL может быть более эффективным. Если это LINQ to Objects, то вы не увидите никакой реальной разницы в производительности. Как всегда, профилируйте свой код и исправьте узкие места, о которых действительно сообщается, вместо того, чтобы пытаться предсказать оптимизацию заранее.

Кристиан Хейтер
источник
1
Почему чрезвычайно большой набор данных имеет значение? Только потому, что крошечная стоимость лямбд в конечном итоге сложится?
BlueRaja - Дэнни Пфлюгофт
1
@ BlueRaja-DannyPflughoeft: Да, вы правы, в этом примере нет никакой дополнительной алгоритмической сложности, кроме исходного кода. Я удалил фразу.
Кристиан Хейтер
Разве это не приводит к двум итерациям над коллекцией? Естественно, второй короче, учитывая, что в нем находятся только допустимые элементы, но вам все равно нужно сделать это дважды, один раз, чтобы отфильтровать элементы, второй раз, чтобы работать с действительными элементами.
Энди
1
@DavidPacker Нет. IEnumerableУправляется только foreachцикл.
Бенджамин Ходжсон
2
@DavidPacker: это именно то, что он делает; большинство методов LINQ to Objects реализуются с использованием блоков итераторов. Приведенный выше пример кода будет проходить через коллекцию ровно один раз, выполняя Whereлямбду и тело цикла (если лямбда возвращает true) один раз для каждого элемента.
Кристиан Хейтер
7

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

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

  • Потеря производительности при вызове делегата составляет порядка нескольких тактов, и, к счастью, мы давно прошли те дни, когда нам приходилось беспокоиться об отдельных тактах.

Если по какой-то причине производительность действительно важна для вас на уровне тактового цикла, используйте List<Item>вместо IList<Item>, чтобы компилятор мог использовать прямые (и встроенные) вызовы вместо виртуальных вызовов, и чтобы итератор List<T>, который на самом деле является а struct, не должен быть в штучной упаковке. Но это действительно мелочи.

Запрос к базе данных - это другая ситуация, поскольку существует (по крайней мере, теоретически) возможность отправки фильтра в СУБД, что значительно повышает производительность: только совпадающие строки будут совершать путешествие из СУБД в вашу программу. Но для этого, я думаю, вам придется использовать linq, я не думаю, что это выражение может быть отправлено в СУБД как есть.

Вы действительно увидите преимущества того if(x) continue;момента, когда вам придется отлаживать этот код: пошаговое переключение между if()s и continues прекрасно работает; Единственный шаг в делегат фильтрации - это боль.

Майк Накис
источник
Это когда что-то не так, и вы хотите просмотреть все элементы и проверить в отладчике, какие из которых имеют Field! = Null, а какие имеют State! = Null; это может быть трудно или невозможно с помощью foreach ... где.
gnasher729
Хороший вопрос с отладкой. В Visual Studio нет ничего плохого, но вы не можете переписать лямбда-выражения во время отладки без перекомпиляции, чего вы избегаете при использовании if(x) continue;.
Паприк
Строго говоря, .Whereвызывается только один раз. То , что можно ссылаться на каждой итерации фильтра делегат (а MoveNextи Currentна счетчику, когда они не получают оптимизированными)
CodesInChaos
@CodesInChaos мне понадобилось немного времени, чтобы понять, о чем ты говоришь, но, конечно, wh00ps, ты прав, строго говоря, .Whereвызывается только один раз. Починил это.
Майк Накис