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

Вызывает ли "foreach" повторное выполнение Linq?

Я работаю впервые с Entity Framework в .NET и пишу LINQ-запросы, чтобы получить информацию от моей модели. Я хотел бы с самого начала программировать в хороших привычках, поэтому я проводил исследования по наилучшему способу писать эти запросы и получать их результаты. К сожалению, при просмотре Stack Exchange я, похоже, столкнулся с двумя противоречивыми объяснениями того, как отложенное/немедленное выполнение работает с LINQ:

  • Функция foreach заставляет запрос выполняться на каждой итерации цикла:

Продемонстрирован вопрос Slow foreach() в запросе LINQ - ToList() значительно повышает производительность - почему это?, подразумевается, что "ToList()" необходимо вызвать для немедленной оценки запроса, поскольку foreach повторно обрабатывает запрос в источнике данных, значительно замедляя работу.

Другим примером является вопрос Представление через сгруппированные результаты linq невероятно медленно, любые советы?, где принятый ответ также подразумевает, что вызов "ToList()" на запрос улучшит производительность.

  • Функция foreach заставляет запрос выполняться один раз и безопасен для использования с LINQ

Демонстрируется вопрос Выполняет ли foreach запрос только один раз?, подразумевается, что foreach заставляет одно перечисление быть установленным и не будет запрашивать источник данных каждый раз.

Продолжение просмотра сайта вызвало множество вопросов, когда "повторное выполнение во время цикла foreach" является виновником проблемы производительности, и множество других ответов, в которых говорится, что foreach будет соответствующим образом захватывать один запрос из источника данных, который означает, что оба объяснения, похоже, имеют силу. Если гипотеза "ToList()" неверна (как и большинство текущих ответов на 2013-06-05 13:51 PM EST, похоже, подразумевает), откуда это заблуждение? Есть ли одно из этих объяснений, которое является точным, а другое - нет или существуют разные обстоятельства, которые могут вызвать запрос LINQ по-другому?

Изменить: В дополнение к принятому ниже ответу, я включил следующий вопрос в отношении Programmers, который очень помог мне понять выполнение запроса, в частности, подводные камни, которые могут привести к множественным ударам данных в течение цикл, который, я думаю, будет полезен другим заинтересованным в этом вопросе: https://softwareengineering.stackexchange.com/info/178218/for-vs-foreach-vs-linq

4b9b3361

Ответ 1

В общем случае LINQ использует отложенное выполнение. Если вы используете такие методы, как First() и FirstOrDefault(), запрос выполняется немедленно. Когда вы делаете что-то вроде:

foreach(string s in MyObjects.Select(x => x.AStringProp))

Результаты извлекаются потоковым образом, что означает один за другим. Каждый раз, когда итератор вызывает MoveNext, проекция применяется к следующему объекту. Если бы у вас был Where, он сначала применил бы фильтр, а затем проекцию.

Если вы сделаете что-то вроде:

List<string> names = People.Select(x => x.Name).ToList();
foreach (string name in names)

Тогда я считаю, что это расточительная операция. ToList() заставит запрос выполнить, перечисляя список People и применяя проекцию x => x.Name. После этого вы снова перечислите список. Поэтому, если у вас нет веских причин иметь данные в списке (а не в IEnumerale), вы просто теряете процессорные циклы.

Вообще говоря, с использованием запроса LINQ в коллекции, которую вы перечислите с помощью foreach, не будет хуже производительности, чем любые другие аналогичные и практичные варианты.

Также стоит отметить, что людям, внедряющим LINQ-провайдеры, рекомендуется использовать общие методы, как в предоставляемых Microsoft провайдерах, но им не требуется. Если бы я должен был написать LINQ to HTML или LINQ для моего поставщика форматов данных, не было бы никакой гарантии, что он будет вести себя таким образом. Возможно, характер данных сделает немедленное выполнение единственным практическим вариантом.

Кроме того, окончательное редактирование; если вы заинтересованы в этом, Jon Skeet С# In Depth очень информативен и замечателен. Мой ответ суммирует несколько страниц книги (надеюсь, с разумной точностью), но если вы хотите получить более подробную информацию о том, как LINQ работает под обложками, это хорошее место для просмотра.

Ответ 2

попробуйте это на LinqPad

void Main()
{
    var testList = Enumerable.Range(1,10);
    var query = testList.Where(x => 
    {
        Console.WriteLine(string.Format("Doing where on {0}", x));
        return x % 2 == 0;
    });
    Console.WriteLine("First foreach starting");
    foreach(var i in query)
    {
        Console.WriteLine(string.Format("Foreached where on {0}", i));
    }

    Console.WriteLine("First foreach ending");
    Console.WriteLine("Second foreach starting");
    foreach(var i in query)
    {
        Console.WriteLine(string.Format("Foreached where on {0} for the second time.", i));
    }
    Console.WriteLine("Second foreach ending");
}

Каждый раз, когда выполняется делегирование делегата, мы видим вывод консоли, поэтому мы можем каждый раз запускать запрос Linq. Теперь, посмотрев на вывод консоли, мы видим, что второй цикл foreach по-прежнему вызывает печать "Doing where on", тем самым показывая, что второе использование foreach действительно приводит к тому, что предложение where запускается снова... потенциально может привести к замедлению.

First foreach starting
Doing where on 1
Doing where on 2
Foreached where on 2
Doing where on 3
Doing where on 4
Foreached where on 4
Doing where on 5
Doing where on 6
Foreached where on 6
Doing where on 7
Doing where on 8
Foreached where on 8
Doing where on 9
Doing where on 10
Foreached where on 10
First foreach ending
Second foreach starting
Doing where on 1
Doing where on 2
Foreached where on 2 for the second time.
Doing where on 3
Doing where on 4
Foreached where on 4 for the second time.
Doing where on 5
Doing where on 6
Foreached where on 6 for the second time.
Doing where on 7
Doing where on 8
Foreached where on 8 for the second time.
Doing where on 9
Doing where on 10
Foreached where on 10 for the second time.
Second foreach ending

Ответ 3

Это зависит от того, как используется запрос Linq.

var q = {some linq query here}

while (true)
{
    foreach(var item in q)
    {
    ...
    }
}

Приведенный выше код будет выполнять запрос Linq несколько раз. Не из-за foreach, а потому, что foreach находится внутри другого цикла, поэтому сам foreach выполняется несколько раз.

Если все потребители запроса linq используют его "тщательно" и избегают немых ошибок, таких как вложенные петли выше, то запрос linq не должен выполняться многократно без необходимости.

Бывают случаи, когда сокращение запроса linq к набору результатов в памяти с использованием ToList() оправдано, но, на мой взгляд, ToList() используется далеко, слишком часто. ToList() почти всегда становится ядовитой таблеткой всякий раз, когда задействованы большие данные, потому что он заставляет весь набор результатов (потенциально миллионы строк) извлекаться в память и кэшироваться, даже если самому внешнему потребителю/перечислителю требуется только 10 строк. Избегайте ToList(), если у вас нет особого обоснования, и вы знаете, что ваши данные никогда не будут большими.

Ответ 4

foreach, сам по себе, выполняет только один раз через свои данные. Фактически, он определенно проходит через него один раз. Вы не можете смотреть вперед или назад или изменять индекс так, как можете, с помощью цикла for.

Однако, если в вашем коде есть несколько foreach, все из которых работают с одним и тем же запросом LINQ, вы можете получить запрос, выполняемый несколько раз. Однако это зависит от данных. Если вы выполняете итерацию с помощью IEnumerable/IQueryable, основанного на LINQ, который представляет запрос к базе данных, он будет запускать этот запрос каждый раз. Если вы выполняете итерацию по List или другой коллекции объектов, она будет запускаться через список каждый раз, но не будет удалять вашу базу данных повторно.

Другими словами, это свойство LINQ, а не свойство foreach.

Ответ 5

Иногда может быть хорошей идеей "кэшировать" запрос LINQ с помощью ToList() или ToArray(), если запрос запрашивается несколько раз в вашем коде.

Но имейте в виду, что "кеширование" по-прежнему вызывает foreach.

Итак, для меня основное правило:

  • если запрос просто используется в одном foreach (и thats it) - тогда я не кэширую запрос
  • если запрос используется в foreach и в некоторых других местах в коде - тогда я кэширую его в var с помощью ToList/ToArray

Ответ 6

Разница заключается в базовом типе. Поскольку LINQ построен поверх IEnumerable (или IQueryable), тот же оператор LINQ может иметь совершенно разные характеристики производительности.

Список всегда будет оперативно реагировать, но для создания списка требуется авансирование.

Итератор также IEnumerable и может использовать любой алгоритм каждый раз, когда он извлекает "следующий" элемент. Это будет быстрее, если вам действительно не нужно проходить полный набор элементов.

Вы можете превратить любой IEnumerable в список, вызывая ToList() на нем и сохраняя результирующий список в локальной переменной. Это желательно, если

  • Вы не зависите от отложенного исполнения.
  • Вам нужно получить доступ к более общим элементам, чем весь набор.
  • Вы можете заплатить авансовые затраты на извлечение и хранение всех элементов.

Ответ 7

Используя LINQ даже без сущностей, вы получите то, что отложенное исполнение действует. Только путем принудительной итерации вычисляется фактическое выражение linq. В этом смысле каждый раз, когда вы используете выражение linq, он будет оцениваться.

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

Это может сделать вашу жизнь проще, но она также может быть болью. Например, если вы запрашиваете все записи из таблицы с помощью выражения linq. Структура сущности загрузит все данные из таблицы. Если позже вы оцениваете одно и то же выражение linq, даже если в момент удаления или добавления записей вы получите тот же результат.

Структура сущности - сложная вещь. Конечно, есть способы сделать это повторно запросом, принимая во внимание изменения, которые он имеет в своей собственной модели памяти и т.п.

Я предлагаю прочитать "структуру сущности программирования" Джулии Лерман. Он рассматривает множество вопросов, подобных тем, которые у вас есть сейчас.