Тривиальный пример "бесконечного" 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
? Я могу полагаться на факт, что они всегда оцениваются "ленивые"? Там опасность состоит в том, чтобы произвести бесконечный цикл?
Да, вам гарантировано, что приведенный выше код будет выполняться лениво. Хотя это выглядит (в вашем коде), как будто вы зацикливаетесь вечно, ваш код на самом деле производит что-то вроде этого:
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; }
}
}
(Очевидно, что это не точно то, что будет сгенерировано, поскольку это довольно специфично для вашего кода, но, тем не менее, похоже и должно показать вам, почему он будет лениво оцениваться).
Вам следует избегать любых жадных функций, которые пытаются читать до конца. Сюда входят перечислимые
расширения, например: Count
, ToArray
/ ToList
и агрегаты Avg
/ Мин
/ Макс
и т. Д.
Нет ничего плохого в бесконечных ленивых списках, но вы должны принимать сознательные решения о том, как с ними обращаться.
Используйте Take
, чтобы ограничить влияние бесконечного цикла, задав верхнюю границу, даже если они вам не нужны.
Пока вы вызываете только ленивые, небуферизованные методы, все будет в порядке. Так что 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;
}
}
Если бы это не было ленивым вычислением, ваш первый пример с самого начала не сработает так, как ожидалось.
Да, ваш код всегда будет работать без бесконечного цикла. Однако кто-то может прийти позже и все испортить. Предположим, они захотят сделать:
var q = Numbers().ToList();
Тогда вам конец! Многие "агрегатные" функции убьют вас, например Max()
.