Что такое генераторы (Generators) в PHP и зачем они нужны?

Ответ

Генераторы в PHP — это специальные функции, которые позволяют итерироваться по набору данных, не создавая весь набор в памяти сразу. Вместо ключевого слова return используется yield. При каждом вызове yield функция приостанавливает своё выполнение и возвращает текущее значение. При следующей итерации выполнение возобновляется с того же места.

Ключевая польза: Экономия оперативной памяти при работе с большими наборами данных (логи, большие файлы, объёмные результаты запросов из БД).

Сравним два подхода:

// ПЛОХО: Функция возвращает массив из миллиона чисел, загружая их все в память.
function getMillionNumbers(): array
{
    $numbers = [];
    for ($i = 0; $i < 1_000_000; $i++) {
        $numbers[] = $i;
    }
    return $numbers; // Массив из 1М элементов создан в памяти
}

foreach (getMillionNumbers() as $number) {
    // В начале цикла массив уже целиком в памяти.
    if ($number > 100) break; // Но мы использовали только 101 элемент!
}
// Пиковое использование памяти: ~130 МБ
// ХОРОШО: Генератор возвращает значения по одному.
function generateMillionNumbers(): Generator
{
    for ($i = 0; $i < 1_000_000; $i++) {
        yield $i; // Возвращает $i и приостанавливается
    }
}

foreach (generateMillionNumbers() as $number) {
    // На каждой итерации в памяти только текущее значение $i.
    if ($number > 100) break;
}
// Пиковое использование памяти: ~1 МБ

Практический пример: Чтение большого файла построчно.

function readLargeFile(string $filename): Generator
{
    $handle = fopen($filename, 'r');
    if (!$handle) {
        throw new RuntimeException("Could not open file: {$filename}");
    }

    try {
        while (($line = fgets($handle)) !== false) {
            yield trim($line); // Отдаём строку по одной
        }
    } finally {
        fclose($handle); // Гарантированное закрытие файла
    }
}

// Использование
foreach (readLargeFile('huge_log.txt') as $lineNumber => $lineContent) {
    // $lineNumber автоматически инкрементируется с каждым yield
    echo "Line {$lineNumber}: {$lineContent}" . PHP_EOL;
    // Файл никогда не загружается в память целиком.
}

Ключевые особенности генераторов в PHP:

  • Функция-генератор возвращает объект внутреннего класса Generator, который реализует интерфейсы Iterator и Traversable.
  • yield может возвращать ключ-значение: yield $key => $value;.
  • Можно делегировать другой генератор с помощью yield from (аналог await в некоторых других языках).
  • Начиная с PHP 7.0, генератор может возвращать финальное значение с помощью return, которое можно получить через $generator->getReturn().

Генераторы — это мощный инструмент для написания эффективного по памяти кода, особенно в контексте обработки потоков данных или реализации ленивых вычислений.

Ответ 18+ 🔞

А, генераторы в PHP! Ну это вообще тема, которая многих вводит в ступор, а на самом деле всё проще пареной репы, если объяснить нормально. Смотри, представь себе такую ситуацию.

Вот ты пишешь функцию, которая должна вернуть, допустим, миллион чисел. Обычный мужик делает так — создаёт массив на миллион элементов, туда всё пихает и возвращает. Ёпта, а потом удивляется, почему скрипт сожрал овердохуища памяти, хотя ему реально нужно было только первые сто чисел проверить. Это как если бы ты пошёл в магазин за бутылкой пива, а купил целый грузовик, потому что "а вдруг понадобится". Хуй с горы, логика-то где?

А генератор — он умный. Он не создаёт всё сразу. Он как ленивый, но эффективный работяга: делает по чуть-чуть, отдаёт тебе результат и присаживается на бутылку покурить, ждёт следующей команды. Вместо return он использует yield. И каждый раз, когда в цикле ты просишь у него следующее значение, он просыпается, делает ещё один шаг и снова засыпает. В рот меня чих-пых, какая же это экономия!

Вот смотри на разницу, тут всё понятно станет.

// ПЛОХОЙ ПУТЬ: Делаем как все, по-старинке. Жрём память как не в себя.
function getMillionNumbers(): array
{
    $numbers = [];
    for ($i = 0; $i < 1_000_000; $i++) {
        $numbers[] = $i;
    }
    return $numbers; // Всё, приехали. Весь этот здоровенный массив уже торчит в оперативке.
}

foreach (getMillionNumbers() as $number) {
    // И вот тут мы, такие умные, смотрим только первые 101 элемент и break.
    if ($number > 100) break;
}
// А память-то уже уплыла, 130 мегабайт как корова языком слизала. **Ядрёна вошь!**
// ХОРОШИЙ ПУТЬ: Включаем голову и делаем через генератор.
function generateMillionNumbers(): Generator
{
    for ($i = 0; $i < 1_000_000; $i++) {
        yield $i; // Отдал цифру, и приостановился. Никаких массивов.
    }
}

foreach (generateMillionNumbers() as $number) {
    // На каждой итерации в памяти только одна циферка $i. Красота!
    if ($number > 100) break;
}
// Памяти съедено — кот наплакал, около мегабайта. Вот это я понимаю — технология!

Самый жирный кейс — это чтение здоровенных файлов, логов там всяких. Раньше люди или file() использовали (кошмар!), или голову ломали. А с генератором — да похуй на размер файла, хоть гигабайтный.

function readLargeFile(string $filename): Generator
{
    $handle = fopen($filename, 'r');
    if (!$handle) {
        throw new RuntimeException("Could not open file: {$filename}");
    }

    try {
        while (($line = fgets($handle)) !== false) {
            yield trim($line); // Вот тебе строчка, держи. Следующую дам, когда попросишь.
        }
    } finally {
        fclose($handle); // Это чтобы даже если что-то пойдёт не так, файл закрылся. Аккуратненько.
    }
}

// Используем
foreach (readLargeFile('huge_log.txt') as $lineNumber => $lineContent) {
    echo "Line {$lineNumber}: {$lineContent}" . PHP_EOL;
    // И файл НИКОГДА не лежит в памяти целиком. Он по строчке подаётся, как суши на конвейере.
}

Что ещё важно знать про этих ебашки-воробушки?

  • Функция-генератор возвращает не массив, а объект класса Generator. Он умеет в итераторы, с ним можно в foreach.
  • Через yield можно отдавать не только значение, но и ключ: yield 'ключ_такой' => 'значение_эдакое'.
  • Есть такая классная штука yield from — это типа делегирование. Можно один генератор из другого вызывать, чтобы не городить код.
  • С PHP 7.0 они ещё и финальное значение через return уметь стали, которое потом getReturn() вытащит. Но это уже для особых ценителей.

Короче, генераторы — это пиздопроебибна штука, когда нужно работать с потоками данных, которые в память не влезают, или просто не хочется грузить её почём зря. Включил — и код сразу стал элегантнее и эффективнее. Сам от себя охуел, когда первый раз понял, как это работает.