Когда foreach встречает дзен

Когда foreach встречает дзен

Леонид Суматохин

Итак, согласно документации:

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

Как оно начиналось

Реализация генераторов появилась в PHP в версии 5.5 в июле 2013 года. Причиной появление этой фичи стало решение проблемы обработки больших наборов данных. Особенно остро стояла проблема загрузки в память всей коллекции данных перед началом её обработки. Даже небольшой скрипт мог вызвать проблему нехватки памяти, если работал с базой данных размером хотя бы в сотни тысяч записей.

Классические способы обхода массивов выглядели следующим образом:

$data = []; // Массив данных
foreach ($resultSet as $row) {
   $data[] = $row;
}
processData($data);

Такой подход приводил к резкому увеличению расхода памяти.

Для решения этой проблемы возникла концепция «ленивого» вычисления. Вместо немедленного формирования полного набора данных стала популярна идея выдавать значения по мере необходимости. Именно эта идея легла в основу будущих нововведений в PHP.

Основные преимущества новой функциональности:

  • Cнижение потребления памяти при работе с большими наборами данных.
  • Упрощённая реализация без необходимости создания громоздких классов итераторов.

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

Таким образом, появление генераторов стало важным этапом развития PHP, направленным на повышение удобства работы с большими объемами данных и улучшение общей производительности приложений.

Как это работает

Когда функция-генератор вызывается, она возвращает объект встроенного класса Generator. Этот объект реализует интерфейс Iterator, который станет однонаправленным объектом итератора и предоставит методы, с помощью которых можно управлять его состоянием, включая передачу в него и возвращения из него значений.

Что умеют генераторы

  • Получение пар «ключ => значение»

Генераторы могут использоваться для построения пар «ключ-значение»

function fruitPairs()
{
    yield 'apple' => 'яблоко';
    yield 'banana' => 'банан';
    yield 'orange' => 'апельсин';
}

foreach (fruitPairs() as $key => $value) {
    echo "Ключ: $key, Значение: $value\n";
}
  • Обратная отправка данных в генераторы

Обратная отправка данных в генераторы (англ. generator delegation) появилась в PHP начиная с версии 5.5, а в версии 7 была расширена возможностями двусторонней коммуникации. Эта технология позволяет передавать данные обратно внутрь генератора непосредственно из внешнего контекста, создавая эффект двунаправленной связи.

Это может понадобится, если возникает необходимость передать дополнительные данные обратно в сам генератор прямо в процессе его выполнения. Например, если нужно изменить поведение генератора динамически или отправить новые входные данные, основываясь на результатах предыдущего шага. 

Что бы это сделать используется метод send();

function myGenerator()
{
    $value = yield "Получите первое значение"; // Ждем первого ввода
    echo "Получено: ", $value, "\n";
    $second_value = yield "Получите второе значение"; // Ждем второго ввода
    echo "Получено: ", $second_value, "\n";
}

$gen = myGenerator();// Запускаем генератор
echo $gen->current(), "\n"; // Получаем сообщение "Получите первое значение"
$gen->send("Первое значение"); // Отправляем первое значение
echo $gen->current(), "\n"; // Получаем сообщение "Получите второе значение"
$gen->send("Второе значение"); // Отправляем второе значение
  • Возврат значения из генератора

Кроме возврата значений при помощи yield, значение из генератора можно вернуть используя инструкцию return;

Return возвращает финальное значение генератора после окончания своей работы. Это достигается добавлением конструкции return в конце функции генератора.

Важный нюанс: финальный результат возвращается не через стандартный механизм return, а через специальное свойство генератора — метод getReturn().

function countDownGenerator($start)
{
    for ($i = $start; $i >= 0; $i--) {
        yield $i;
    }
    return "Финиш!";
}

$gen = countDownGenerator(3);
foreach ($gen as $val) {
    echo $val . "\n";
}
echo $gen->getReturn() . "\n";

Сравнение генераторов с объектами класса Iterator

Главное преимущество генераторов – простота реализации. Просто для сравнения:

  • Итератор
class LineIterator implements Iterator
 {
    protected $fileHandle;
 
    protected $line;
    protected $i;
 
    public function __construct($fileName)
    {
        if (!$this->fileHandle = fopen($fileName, 'r')) {
            throw new RuntimeException('Невозможно открыть файл "' . $fileName . '"');
        }
    }
 
    public function rewind()
    {
         fseek($this->fileHandle, 0);
         $this->line = fgets($this->fileHandle);
         $this->i = 0;
    }
 
    public function valid()
    {
        return false !== $this->line;
    }
 
    public function current()
    {
        return $this->line;
    }
 
    public function key()
    {
        return $this->i;
    }
 
    public function next()
    {
        if (false !== $this->line) {
             $this->line = fgets($this->fileHandle);
             $this->i++;
        }
    }
 
    public function __destruct()
    {
         fclose($this->fileHandle);
    }
 }
  • Генератор
function getLinesFromFile($fileName)
 {
    if (!$fileHandle = fopen($fileName, 'r')) {
        return;
    }
 
    while (false !== $line = fgets($fileHandle)) {
        yield $line;
    }
 
     fclose($fileHandle);
 }

По-моему слова излишни.


И, как итог тезисно о генераторах:

  • Это не новая функциональность, а развитие существующей;
  • Увеличенная скорость обработки большого кол-ва данных;
  • Генераторы однонаправленны и работают только «вперёд»;
  • Генераторы позволяют строить более понятные кодовые конструкции;


Источники: О генераторах, об итераторах, о скорости, о нюансах, хабр


Report Page