2012-04-25 1 views
7

Предположим, у меня есть следующий код:Вычисляет ли значения кэша LINQ?

var X = XElement.Parse (@" 
    <ROOT> 
     <MUL v='2' /> 
     <MUL v='3' /> 
    </ROOT> 
"); 
Enumerable.Range (1, 100) 
    .Select (s => X.Elements() 
     .Select (t => Int32.Parse (t.Attribute ("v").Value)) 
     .Aggregate (s, (t, u) => t * u) 
    ) 
    .ToList() 
    .ForEach (s => Console.WriteLine (s)); 

Что такое среда .NET на самом деле делает здесь? Является ли это синтаксический анализ и преобразование атрибутов в целые числа каждый из 100 раз или он достаточно умен, чтобы понять, что он должен кэшировать проанализированные значения и не повторять вычисления для каждого элемента в диапазоне?

Кроме того, как бы я сам разобрался в этом?

Заранее за вашу помощь.

+2

«Как бы я хотел разобраться в чем-то подобном себе» - лучший снимок - это изучение ИЛ, созданного из этого кода. – Andrey

+1

Вы можете установить точку останова отладчика в методе Parse() и посмотреть, как часто она попадает. –

ответ

2

Прошло некоторое время с тех пор, как я прорыл этот код, но, IIRC, работает Select - просто кешировать Func, который вы его поставляете, и запускать его по исходной коллекции по одному. Таким образом, для каждого элемента во внешнем диапазоне он будет запускать внутреннюю последовательность Select/Aggregate, как если бы это было в первый раз. Никакого встроенного кэширования не происходит - вам придется реализовать это самостоятельно в выражениях.

Если вы хотите, чтобы понять это самостоятельно, у вас есть три основных варианта:

  1. компилировать код и использовать ildasm для просмотра IL; это наиболее точно, но, особенно с lambdas и закрытиями, то, что вы получаете от IL, может выглядеть не так, как вы добавляете в компилятор C#.
  2. Используйте что-то вроде dotPeek для декомпиляции System.Linq.dll в C#; опять же, то, что вы получаете из этих инструментов, может лишь приблизительно напоминать исходный код, но по крайней мере это будет C# (и dotPeek, в частности, делает довольно хорошую работу, и является бесплатным.)
  3. Мои предпочтения - загрузите .NET 4.0 Reference Source и ищите сами; это то, для чего это нужно :) Вы должны просто доверять MS, чтобы исходный источник соответствовал фактическому источнику, используемому для создания двоичных файлов, но я не вижу веских оснований сомневаться в них.
  4. Как указано @AllonGuralnek, вы можете установить точки останова на определенные лямбда-выражения в пределах одной строки; поместите курсор где-нибудь внутри тела лямбды и нажмите F9, и он будет останавливаться только на лямбда. (Если вы сделаете это неправильно, это будет выделить всю строку в виде точек останова цвета, если вы сделаете это правильно, это будет просто выделить лямбда.)
+0

Спасибо за ответ. Я попробую первый и третий методы. – Shredderroy

+2

4. Поместите курсор после '=>' и нажмите F9. Это поставит точку останова внутри лямбды и сломается, когда она достигнет ее. Повторите для каждой лямбды, и вы получите хороший след того, что называется когда. –

+0

@AllonGuralnek, это хороший момент, я, как правило, забываю о breakpointing lambdas, потому что я обычно использую мышь, чтобы установить их :) –

4

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

вытягивать из IEnumerable<T> делаются на foreach заявлении, которое на самом деле является синтаксическим сахаром для получения перечислителя по телефону IEnumerable<T>.GetEnumerator() и неоднократно вызывая IEnumerator<T>.MoveNext() тянуть значение.

LINQ операторы, такие как ToList(), ToArray(), ToDictionary() и ToLookup() обертывания foreach заявления, чтобы эти методы будут делать тянуть. То же самое можно сказать и о таких операторах, как Aggregate(), Count() и First(). Эти методы имеют общий характер, что они создают один результат, который должен быть создан путем выполнения инструкции foreach.

Многие операторы LINQ производят новую последовательность IEnumerable<T>. Когда элемент вытягивается из результирующей последовательности, оператор вытягивает один или несколько элементов из исходной последовательности. Оператор Select() является наиболее очевидным примером, но другие примеры: SelectMany(), Where(), Concat(), Union(), Distinct(), Skip() и Take(). Эти операторы не выполняют кэширование. Когда затем N-й элемент вытягивается из Select(), он вытягивает N-й элемент из исходной последовательности, применяет проекцию с помощью прилагаемого действия и возвращает ее. Ничего секретного здесь не происходит.

Другие операторы LINQ также производят новые последовательности IEnumerable<T>, но они реализованы путем фактического вытягивания всей последовательности источников, выполнения их работы и последующего создания новой последовательности. Эти методы включают Reverse(), OrderBy() и GroupBy(). Однако притяжение оператора выполняется только тогда, когда сам оператор вытаскивается, что означает, что вам все еще нужен цикл foreach «в конце» оператора LINQ, прежде чем что-либо будет выполнено. Вы можете утверждать, что эти операторы используют кеш, потому что они сразу же извлекают всю исходную последовательность. Тем не менее, этот кеш создается каждый раз, когда оператор повторяется, так что это действительно деталь реализации, а не то, что волшебным образом обнаружит, что вы применяете одну и ту же операцию OrderBy() несколько раз к той же последовательности.


В вашем примере ToList() будет тянуть. Действие во внешнем Select будет выполняться 100 раз. Каждый раз, когда это действие выполняется, Aggregate() выполнит еще одно нажатие, которое будет анализировать атрибуты XML. В общей сложности ваш код будет звонить Int32.Parse() 200 раз.

Вы можете улучшить это, потянув атрибуты один раз вместо того, чтобы на каждой итерации:

var X = XElement.Parse (@" 
    <ROOT> 
     <MUL v='2' /> 
     <MUL v='3' /> 
    </ROOT> 
") 
.Elements() 
.Select (t => Int32.Parse (t.Attribute ("v").Value)) 
.ToList(); 
Enumerable.Range (1, 100) 
    .Select (s => x.Aggregate (s, (t, u) => t * u)) 
    .ToList() 
    .ForEach (s => Console.WriteLine (s)); 

Теперь Int32.Parse() вызывалась только в 2 раза. Однако стоимость заключается в том, что список значений атрибутов должен быть выделен, сохранен и в конечном итоге собран из мусора. (Не очень важно, когда список содержит два элемента.)

Обратите внимание, что если вы забудете первый ToList(), который вытаскивает атрибуты, код все равно будет работать, но с теми же характеристиками, что и исходный код. Для хранения атрибутов не используется пробел, но они обрабатываются на каждой итерации.

+0

Большое спасибо за подробный ответ. – Shredderroy