Может кто-нибудь объяснить этот ленивый код оценки? - PullRequest
6 голосов
/ 29 апреля 2010

Итак, этот вопрос был только что задан на SO:

Как справиться с "бесконечным" IEnumerable?

Мой пример кода:

public static void Main(string[] args)
{
    foreach (var item in Numbers().Take(10))
        Console.WriteLine(item);
    Console.ReadKey();
}

public static IEnumerable<int> Numbers()
{
    int x = 0;
    while (true)
        yield return x++;
}

Может кто-нибудь объяснить, почему это лениво оценивается? Я посмотрел этот код в Reflector, и я более запутался, чем когда я начал.

Выходы отражателя:

public static IEnumerable<int> Numbers()
{
    return new <Numbers>d__0(-2);
}

Для метода чисел, который, похоже, сгенерировал новый тип для этого выражения:

[DebuggerHidden]
public <Numbers>d__0(int <>1__state)
{
    this.<>1__state = <>1__state;
    this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
}

Это не имеет смысла для меня. Я бы предположил, что это бесконечный цикл, пока не собрал этот код и не выполнил его сам.

РЕДАКТИРОВАТЬ : Итак, теперь я понимаю, что .Take () может сказать foreach, что перечисление «закончилось», хотя на самом деле это не так, но не следует вызывать Numbers () в его целиком, прежде чем приковать цепочку к Take ()? Результат Take - это то, что на самом деле перечисляется, верно? Но как выполняется выполнение Take, когда Numbers еще не полностью оценен?

EDIT2 : Так это просто особый трюк с компилятором, применяемый с помощью ключевого слова yield?

Ответы [ 3 ]

2 голосов
/ 29 апреля 2010

Это должно с:

  • Что делает iEnumerable при вызове определенных методов
  • Характер перечисления и заявление Доходность

Когда вы перечисляете по любому виду IEnumerable, класс дает вам следующий элемент, который он вам даст. Он ничего не делает с всеми своими предметами, он просто дает вам следующий предмет. Он решает, каким будет этот предмет. (Например, некоторые коллекции упорядочены, некоторые нет. Некоторые не гарантируют определенный порядок, но, похоже, всегда возвращают их в том же порядке, в котором вы их поместили.).

Метод расширения IEnumerable Take() будет перечислять 10 раз, получая первые 10 элементов. Вы можете сделать Take(100000000), и это даст вам много цифр. Но вы просто делаете Take(10). Он просто просит Numbers() для следующего элемента. , , 10 раз.

Каждый из этих 10 предметов, Numbers дает следующий предмет. Чтобы понять, как, вам нужно прочитать инструкцию Yield. Это синтаксический сахар для чего-то более сложного. Выход очень мощный. (Я разработчик VB и очень раздражен тем, что у меня его до сих пор нет.) Это не функция; это ключевое слово с определенными ограничениями. И это делает определение перечислителя намного проще, чем могло бы быть.

Другие методы расширения IEnumerable всегда перебирают каждый элемент. Вызов .AsList взорвет его. Используя его, большинство запросов LINQ взорвали бы его.

1 голос
/ 29 апреля 2010

Причина, по которой это не бесконечный цикл, состоит в том, что вы перечисляете только 10 раз в соответствии с использованием вызова Take (10) Linq. Теперь, если вы написали код что-то вроде:

foreach (var item in Numbers())
{
}

Теперь это бесконечный цикл, потому что ваш перечислитель всегда будет возвращать новое значение. Компилятор C # берет этот код и превращает его в конечный автомат. Если у вашего перечислителя нет пункта охраны, чтобы прервать выполнение, то вызывающий должен сделать это в вашем примере.

Причина, по которой код является ленивым, также является причиной того, почему код работает. По сути, Take возвращает первый элемент, затем ваше приложение потребляет, затем требуется еще один, пока не будет принято 10 элементов.

Редактировать

Это на самом деле не имеет ничего общего с добавлением дубля. Они называются итераторами. Компилятор C # выполняет сложное преобразование вашего кода, создавая перечислитель из вашего метода. Я рекомендую прочесть об этом, но в основном (и это может быть не на 100% точно) ваш код будет вводить метод Numbers, который вы можете представить как инициализирующий конечный автомат.

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

У Эрика Липперта есть отличная серия о разных аспектах Итераторов

0 голосов
/ 29 апреля 2010

По сути, ваша функция Numbers () создает перечислитель.
Foreach будет проверять в каждой итерации, достиг ли конец перечислитель, и если нет, то будет продолжаться. Ваш пракулярный перечислитель никогда не закончится, но это не имеет значения. Это лениво оценивается.
Перечислитель сгенерирует результаты «вживую».
Это означает, что если вы напишите .Take (3), цикл будет выполняться только три раза. В перечислителе все еще были бы некоторые элементы, «оставленные» в нем, но они не будут генерироваться, так как в настоящее время они не нужны ни одному методу.
Если бы вы попытались сгенерировать все числа от 0 до бесконечности, как подразумевает функция, и вернуть их все сразу, эта программа, которая использует только 10 из них, будет намного, намного медленнее. В этом преимущество ленивой оценки - то, что никогда не используется, никогда не рассчитывается.

...