Засада с LINQ

В процессе реализации программы, о которой я писал в прошлом посте, я напоролся на одну забавную засаду.

Дело в том, что слова из файла я получаю так (упрощенно):

   24 while (true)

   25 {

   26     var word = file.ReadLine();

   27     if (word.Length == length) yield return word; // нашли слово нужной длины

   28 }

Никаких дополнительных телодвижений я не делал, так как неоднократно слышал, что в LINQ изначально реализован шаблон lazy load. Каково же было мое удивление, когда я потом увидел, что считывание файла идет каждый раз при получении значения из этого набора данных! Производительность просела так, что это было визуально заметно, я пожал плечами и, не разбираясь, прилепил один раз вызов .ToArray(), чтобы гарантировано считать слова, запомнить их и больше в файл не лазать.

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

   59 generation = generation.SelectMany(m => Step(m)); // Получим следующее поколение


Казалось бы обычное получение набора элементов следующего поколения. Но хотя эта команда давала правильные на первый взгляд результаты, любое следующее обращение к этим элементам выдавало пустое множество! То есть если вывести значение .Count() в консоль, то выдавалось число элементов, а если потом поглядеть результаты в отладке, то значений не было. Если выдать значение .Count() два раза в консоль, то во втором случае печатался 0!

Я впал в задумчивость, а потом в глубокую отладку. Благо рефлектор позволяет отладчиком трассировать внутренние вызовы стандартной библиотеки. Отладкой я быстро (часа через полтора) нашел корень зла: оказалось, что никакого lazy load в LINQ нет. Точнее есть lazy, а нет load. :) То есть данные не загружаются при создании LINQ-запроса, но даже когда требуются значения, никакого сохранения данных для ускорения последующего доступа не происходит! То есть мой метод .Step() (а ранее метод чтения из файла) вызывался каждый раз при обращении к набору. А это все ломало, так как внутри этого метода правились элементы колекции так, что повторно они не выбирались!

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