Как обрабатывать "бесконечный" IEnumerable?
Тривиальный пример "бесконечного" IEnumerable будет
IEnumerable<int> Numbers() {
int i=0;
while(true) {
yield return unchecked(i++);
}
}
Я знаю, что
foreach(int i in Numbers().Take(10)) {
Console.WriteLine(i);
}
и
var q = Numbers();
foreach(int i in q.Take(10)) {
Console.WriteLine(i);
}
оба работают нормально (и распечатывают номер 0-9).
Но есть ли какие-либо подводные камни при копировании или обработке выражений типа q
? Могу ли я полагаться на то, что они всегда оцениваются "ленивыми"? Есть ли опасность создать бесконечный цикл?
Ответы
Ответ 1
Да, вы гарантированно выполняете ленивый код. Хотя он выглядит (в вашем коде), как будто бы вы навсегда зацикливаетесь, ваш код действительно производит что-то вроде этого:
IEnumerable<int> Numbers()
{
return new PrivateNumbersEnumerable();
}
private class PrivateNumbersEnumerable : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
return new PrivateNumbersEnumerator();
}
}
private class PrivateNumbersEnumerator : IEnumerator<int>
{
private int i;
public bool MoveNext() { i++; return true; }
public int Current
{
get { return i; }
}
}
(Это, очевидно, не совсем то, что будет сгенерировано, поскольку это довольно специфично для вашего кода, но оно, тем не менее, похоже и должно показать вам, почему это будет лениво оценено).
Ответ 2
Пока вы называете только ленивые, небуферизованные методы, все должно быть хорошо. Так что Skip
, Take
, Select
и т.д. Все в порядке. Однако Min
, Count
, OrderBy
и т.д. Сойдут с ума.
Он может работать, но вам нужно быть осторожным. Или добавьте Take(somethingFinite)
в качестве меры безопасности (или какой-либо другой пользовательский метод расширения, который генерирует исключение после слишком большого количества данных).
Например:
public static IEnumerable<T> SanityCheck<T>(this IEnumerable<T> data, int max) {
int i = 0;
foreach(T item in data) {
if(++i >= max) throw new InvalidOperationException();
yield return item;
}
}
Ответ 3
Вам придется избегать жадных функций, которые пытаются прочитать до конца. Это включает в себя расширения Enumerable
, такие как: Count
, ToArray
/ToList
и агрегаты Avg
/Min
/Max
и т.д.
Нет ничего плохого в бесконечных ленивых списках, но вы должны принимать осознанные решения о том, как с ними обращаться.
Используйте Take
, чтобы ограничить влияние бесконечного цикла, установив верхнюю границу, даже если вам не нужны все.
Ответ 4
Да, ваш код всегда будет работать без бесконечного цикла. Кто-то может прийти позже, и все испортится. Предположим, что они хотят:
var q = Numbers().ToList();
Тогда ты хочешь! Многие "агрегатные" функции убьют вас, например Max()
.
Ответ 5
Если бы это была не ленивая оценка, ваш первый пример не будет работать, как ожидалось, в первую очередь.