Причина, по которой это не бесконечный цикл, состоит в том, что вы перечисляете только 10 раз в соответствии с использованием вызова Take (10) Linq. Теперь, если вы написали код что-то вроде:
foreach (var item in Numbers())
{
}
Теперь это бесконечный цикл, потому что ваш перечислитель всегда будет возвращать новое значение. Компилятор C # берет этот код и превращает его в конечный автомат. Если у вашего перечислителя нет пункта охраны, чтобы прервать выполнение, то вызывающий должен сделать это в вашем примере.
Причина, по которой код является ленивым, также является причиной того, почему код работает. По сути, Take возвращает первый элемент, затем ваше приложение потребляет, затем требуется еще один, пока не будет принято 10 элементов.
Редактировать
Это на самом деле не имеет ничего общего с добавлением дубля. Они называются итераторами. Компилятор C # выполняет сложное преобразование вашего кода, создавая перечислитель из вашего метода. Я рекомендую прочесть об этом, но в основном (и это может быть не на 100% точно) ваш код будет вводить метод Numbers, который вы можете представить как инициализирующий конечный автомат.
Как только ваш код достигает доходности, вы, по сути, говорите, что Numbers () прекращают выполнение, возвращают им этот результат, а затем, когда они просят возобновить выполнение следующего элемента на следующей строке после возврата урожая.
У Эрика Липперта есть отличная серия о разных аспектах Итераторов