Lazy LINQ
Подивившись жизненной несправедливости в прошлый раз, я задумался, что прикрутить ленивую загрузку в LINQ запрос должно быть не очень-то и сложно.
Основная идея проста: сделать дополнительный метод расширения, который бы выдавал не сразу данные по yield return, а возвращал бы какой-то объект, кеширующий после первого обращения данные. Отложенность загрузки хотелось сохранить, грузить при первом обращении всю последовательность также не хотелось. Поэтому я создал class LazyEnumerable<T>: IEnumerable<T>, в котором хранил исходную последовательность, объект-перечислитель для нее и уже загруженные данные в виде обычного List<T>.
Вся логика была заключена в собственном объекте-перечислителе (enumerator) для этой последовательности. В конструктор энумератора я передавал исходную энумератор исходной последовательности и список уже загруженных элементов. Сперва я при обращении выдавал все элементы кеша, а когда они кончались, начинал выдавать элементы последовательности, заполняя попутно кеш:
public bool MoveNext() { if (!_cacheEnded && _cacheEnumerator.MoveNext()) { return true; // Идем по сохраненной последовательности } else { // Сохраненная последовательность кончилась, идем по основноц _cacheEnded = true; // Закончился кеш bool res = _enumerator.MoveNext(); if (res) _cache.Add(this.Current); return res; } }public T Current { get { if (!_cacheEnded) return _cacheEnumerator.Current; else return _enumerator.Current;
}
}
При реализации в очередной раз порадовался, насколько шаблоны проектирования облегчают разработку. Идею можно сформулировать в понятной для любого разработчика формулировке: "Выдавать сперва энумератор сохраненной, а потом исходной последовательности." Хотя надо заметить, что получившийся код получился не супер-читаемым как минимум из-за того, что пришлось реализовывать и IEnumerator<T> и IEnumerator. А это еще не добавлена потокобезопасность, например. Так что подобные методы массово писать без реальной необходимости я бы не стал.
Исходные коды для этого поста можно скачать отсюда.