Кто-то может объяснить этот код отложенных вычислений?

Так, на этом вопросе просто задали ТАК:

Как обработать "бесконечный" IEnumerable?

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

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

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

Кто-то может объяснить, почему это лениво оцененный? Я искал этот код в Отражателе, и я более смущен чем тогда, когда я начал.

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

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

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

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

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

Править: Таким образом, я понимаю теперь, когда.Take () может сказать foreach, что перечисление 'закончилось', когда это действительно не имеет, но не были должны Числа () быть призванными, это - полнота прежде, чем объединить в цепочку вперед к Взятию ()? Результат Взятия - то, что на самом деле перечисляется, корректное? Но как состоит в том выполнение Взятия, когда Числа не полностью оценили?

EDIT2: Таким образом, это - просто определенный прием компилятора, осуществленный ключевым словом 'урожая'?

6
задан Community 23 May 2017 в 12:07
поделиться

3 ответа

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

foreach (var item in Numbers())
{
}

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

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

Править

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

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

Эрик Липперт написал большую серию о разных аспектах итераторов

1
ответ дан 17 December 2019 в 18:11
поделиться

Это связано с:

  • Что делает iEnumerable при вызове определенных методов
  • Природа перечисления и Yield оператор

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

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

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

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

2
ответ дан 17 December 2019 в 18:11
поделиться

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

0
ответ дан 17 December 2019 в 18:11
поделиться
Другие вопросы по тегам:

Похожие вопросы: