Должен ли я всегда вызывать .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