Должен ли я всегда вызывать .ToArray в результатах запроса LINQ, возвращаемых функцией?

Я столкнулся с довольно многими случаями ошибок Collection was modified; enumeration operation may not execute при возврате результатов запроса LINQ в функции, вроде этого... (Я должен добавить, что функция действует как реализация интерфейса и результатов оставьте этот модуль для использования в другом.)

Public Function GetTheFuzzyFuzzbuzzes() As IEnumerable(of FuzzBuzz) _
    Implements IFoo.GetTheFuzzyFuzzBuzzes

    Return mySecretDataSource.Where(Function(x) x.IsFuzzy)
End Function

Должен ли я, как правило, всегда вызывать .ToArray при возврате результата запроса LINQ в функцию или свойство getter, если базовые данные могут быть изменены? Я знаю, что в этом есть что-то полезное, но я чувствую, что это безопасно, и поэтому всегда нужно делать, чтобы избежать проблем с временными связями.

Edit:

Позвольте мне лучше описать проблемную область.

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

... все это означает, что у нас много чего происходит очень асинхронно.

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

Ответ 1

Нет, я бы не стал править этим.

Я понимаю вашу озабоченность. Вызывающая сторона может не знать, что ее действия влияют на результаты запроса.

Есть несколько случаев, когда вы действительно не можете этого сделать:

  • Есть примеры, когда это приведет к выходу из памяти, например, с бесконечными перечислениями, или в счетчике, который производит новое вычисляемое изображение на каждой итерации. (У меня есть оба).
  • Если вы используете Any() или First() в своих запросах. Оба требуют только чтения первого элемента. Вся остальная работа выполняется напрасно.
  • Если вы ожидаете, что Enumerables будет соединен цепью с помощью труб/фильтров. Материализация промежуточных результатов - это только дополнительная стоимость.

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

При написании программного обеспечения звучит привлекательно иметь правила, в которых говорится: "Когда вам нужно выбирать между X и Y, всегда делайте X". Я не верю, что есть такие правила. Возможно, в 15% вы действительно должны делать X, в 5% вам определенно нужно делать Y, а для остальных случаев это просто не имеет значения.

Для оставшихся 80% ничего не может быть подходящим. Если вы вставляете ToArray() всюду, код неправомерно указывает на то, что была причина, по которой это сделано.

Ответ 2

В общем случае вы не должны всегда вызывать .ToArray или .ToList при возврате результата запроса LINQ.

Оба .ToArray и .ToList являются "жадными" (противоположными ленивым) операциями, которые фактически выполняют запрос к источнику ваших данных. И подходящее место и время для их вызова - это архитектурное решение. Например, вы могли бы установить правило в своем проекте для материализации всех запросов linq внутри уровня доступа к данным и, таким образом, обработать все исключения уровня данных. Или чтобы они не выполнялись до тех пор, пока это возможно, и только получить требуемые данные с самого конца. И есть много других деталей, связанных с этой темой.

Но вызывать или не вызывать .ToArray при возврате результата из вашей функции - это не вопрос, и у него нет ответа, пока вы не представите более подробный образец.

Ответ 3

Если вы собираетесь вернуть IEnumerable (или IQueryable или что-то вроде тех, которые не являются самодостаточными), ограничения на то, когда это можно назвать, что можно сделать с ним или как долго это может быть должны быть четко прописаны.

По этим причинам я рекомендую возвращать FuzzBuzz[] вместо IEnumerable<FuzzBuzz>, если это какой-то API (т.е. между слоями). Если это часть внутренней реализации класса/модуля, проще обосновать оцениваемый с задержкой IEnumerable<FuzzBuzz>, но все же разумно использовать массив.

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

Ответ 4

"Как правило", "Нет", вы не должны всегда вызывать ToList/ToArray. В противном случае запросы, такие как myData.GetSomeSubset().WhereOtherCondition().Join(otherdata), тратят кучу времени, выделяя временные буферы для каждого связанного вызова. Но LINQ работает лучше всего с неизменяемыми коллекциями. Возможно, вы захотите быть более осторожными в том месте, где вы изменяете mySecretDataSource.

В частности, если ваш код всегда структурирован вокруг частой модификации вашего источника данных, это звучит как повод для надежного возвращения массива вместо IEnumerable