2014-02-13 4 views
4

Я знаю, что IEnumerable ленива, но я не понимаю, почему Enumerable.Range получает итерация здесь дважды:Почему Enumerable.Range повторяется дважды?

using System; 
using System.Collections.Generic; 
using System.Linq; 

namespace ConsoleApplication 
{ 
    class Program 
    { 
     static int GetOne(int i) 
     { 
      return i/2; 
     } 

     static IEnumerable<int> GetAll(int n) 
     { 
      var items = Enumerable.Range(0, n).Select((i) => 
      { 
       Console.WriteLine("getting item: " + i); 
       return GetOne(i); 
      }); 

      var data = items.Select(item => item * 2); 

      // data.Count does NOT causes another re-iteration 
      Console.WriteLine("first: items: " + data.Count()); 
      return data; 
     } 

     static void Main() 
     { 
      var data = GetAll(3); 

      // data.Count DOES cause another re-iteration 
      Console.WriteLine("second: items: " + data.Count()); 
      Console.ReadLine(); 
     } 
    } 
} 

Результат:

getting item: 0 
getting item: 1 
getting item: 2 
first: items: 3 
getting item: 0 
getting item: 1 
getting item: 2 
second: items: 3 

Почему не получить повторно итерацию в «первой «дело, но делает в« втором »?

ответ

5

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

На вершине вещей, как Array или List<T> это не такая проблема, но когда реализация на запросе, или более сложной yield return структуры или какой-либо другой набор кода (например, Enumerable.Range), он может потенциально дорогой.

Вот почему ReSharper делает такие вещи, как предупредить вас о множественном перечислении.

Если вам нужно запомнить результаты Count, используйте переменную. Если вы хотите защитить от перечисления дорогого источника, вы, как правило, делаете что-то вроде var myCachedValues = myEnumerable.ToArray(), а затем переходите на итерацию массива (тем самым гарантируя только одну итерацию).

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

+0

Почему она не получить повторно итерацию сразу после первого выступа ' var data = items.Select (item => item * 2) '? Происходит ли кеширование? – avo

+1

@avo 'Select' является отложенным методом, поэтому он будет выполнять только итерацию, когда вы на самом деле перечислите« данные ». 'Count' не откладывается, как и другие методы агрегирования (' All', 'Sum',' GroupBy' и т. Д.). –

2

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

+0

Это не так. Если я буду следовать за вами правильно, должно быть три повтора: один для начального 'Enumerable.Range(). Select()' и один для каждого 'data.Count()', правильно? – avo

+2

@avo, нет не правильно, первый 'Select' не заставляет итерацию, это откладывается. Фактически, эта проекция повторяется, когда вы вызываете 'Count' оба раза. –

+0

Понял, спасибо. – avo

1

Из-за отложенного исполнения, так что если вы хотите выполнить только один раз, измените строку

var data = items.Select(item => item * 2); 

в

var data = items.Select(item => item * 2).ToList(); 
Смежные вопросы