Кэширование при использовании выражений запросов? - PullRequest
1 голос
/ 09 июля 2009

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

IEnumerable<int> collection = from i in integers where i % 2 == 0 select i;

Он будет пересчитываться каждый раз при обращении к collection?

Если так, какова общая практика, чтобы иметь дело с этим? Преобразовать в новую коллекцию?

Кроме того, почему дизайнеры C # выбрали именно этот путь, а не то, где он кэширует результат в коллекции после первого доступа к коллекции?

Кроме того, как среда выполнения узнает, что collection ведет себя таким образом (откладывает выполнение), в отличие от другого IEnumerable<T>, который я мог бы создать, используя List<T>, который не откладывает выполнение?


Edit:

А как насчет таких случаев:

List<int> ints = new List<int> ( ) { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };

var even = from i in ints where i % 2 == 0 select i;

ints.AddRange ( new List<int> ( ) { 10, 20, 30, 40, 50 } );

foreach ( int i in even )
{
    Console.WriteLine ( i );
}

Выход:

2, 4, 6, 8, 0, 10, 20, 30, 40, 50

Кешируя, будет ли поведение более ожидаемым?

Ответы [ 2 ]

2 голосов
/ 09 июля 2009

Мне проще думать о IEnumerable<T> как о последовательности, а не как о коллекции, что является терминологией, используемой F #. По сути, все обещания IEnumerable состоят в том, что он может вернуть IEnumerator, что само по себе обеспечивает простой контракт, который, как оказалось, имеет причудливую языковую конструкцию, облегчающую использование: foreach.

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

IEnumerable<int> collection = integers.Where(i => i % 2 == 0);

... воспринимайте collection как последовательность значений в integers, которые являются четными, а не «набор» таких целых чисел. Я бы даже переименовал collection во что-то более точное, например evenIntegers.

Чтобы ответить на ваши конкретные вопросы:

  1. будет ли он пересчитываться каждый раз при доступе к коллекции?

    Каждый вызов collection.GetEnumerator() возвращает нового счетчика, да. Фактически, если вы перечислите collection, а затем обновите integers, перечисление collection снова даст другой результат. А вызов дополнительных отложенных операторов LINQ будет просто возвращать новые последовательности, которые работают с отфильтрованной последовательностью collection, опять же без фактического вычисления чего-либо до тех пор, пока они не будут перечислены.

  2. Если так, какова общая практика, чтобы иметь дело с этим? Преобразовать в новую коллекцию?

    Как правило, вы должны отложить выполнение как можно дольше. Объединяя в цепочку различные методы LINQ, можно создать действительно «умную» последовательность, которая на самом деле ничего не сделает, пока вы не используете foreach (или First, Last, агрегацию типа Count и т. Д.).

  3. Кроме того, почему разработчики C # выбрали именно этот путь, а не то, где он кэширует результат в коллекцию после первого доступа к коллекции?

    Различные непосредственные операторы (ToArray, ToList, ToDictionary, ToLookup) были предоставлены для поддержки сценариев, когда требуется сбор в памяти. Отсроченную последовательность легко кэшировать; невозможно отложить кеширование последовательности.

  4. Также, как среда выполнения узнает, что коллекция ведет себя таким образом (откладывает выполнение), в отличие от другого IEnumerable, который я мог бы создать, используя List, который не откладывает выполнение?

    Как я уже говорил, среда выполнения ничего не знает о том, как поведет себя конкретный IEnumerable<T>. Он просто знает, что предоставит перечислитель. Это зависит от конкретного исполнителя (например, List<T>), чтобы решить, как он хочет вести себя.

2 голосов
/ 09 июля 2009

Да, каждый раз будет пересчитываться.

Если вы хотите его кэшировать, используйте ToArray() в результате (или ToList(), если вы хотите добавить новые элементы позже).

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

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

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

F # на самом деле имеет стандартный тип для такой ленивой последовательности кэширования, который называется LazyList - вы можете заключить в него любой IEnumerable, чтобы получить желаемую семантику со всеми оговорками, описанными выше.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...