Статьи

Генераторы в PHP

Если вы следовали моим предыдущим постам об итераторах, то вы знаете, что итерация является важной концепцией программирования, но реализация необходимых интерфейсов для создания итерируемого объекта может быть в лучшем случае хлопотной из-за необходимого количества стандартного кода. С выпуском PHP 5.5 у нас наконец появились генераторы!

В этой статье мы рассмотрим генераторы, которые обеспечивают простой способ реализации простых итераторов без дополнительных затрат и сложности интерфейса Iterator .

Как работают генераторы?

Согласно Википедии, генератор «очень похож на функцию, которая возвращает массив, в том смысле, что генератор имеет параметры, может вызываться и генерирует последовательность значений». Генератор — это обычная функция, но вместо возврата значения он выдает столько значений, сколько ему нужно. Это похоже на функцию, но действует как итератор.

Генераторы используют ключевое слово yield вместо return . Он действует аналогично return тем, что возвращает значение вызывающей функции, но вместо удаления функции из стека yield сохраняет свое состояние. Это позволяет функции продолжать с того места, где она была, когда она вызывается снова. Фактически, вы не можете вернуть значение из генератора, хотя вы можете использовать return без значения, чтобы прекратить его выполнение.

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

 <?php function nums() { echo "The generator has startedn"; for ($i = 0; $i < 5; ++$i) { yield $i; echo "Yielded $in"; } echo "The generator has endedn"; } foreach (nums() as $v); 

Вывод приведенного выше кода будет:

  Генератор запущен
 Уступил 0
 Дается 1
 Уступил 2
 Получено 3
 Дала 4
 Генератор закончился 

Наш первый генератор

Генераторы не являются новой концепцией и уже существуют в таких языках, как C #, Python, JavaScript и Ruby (перечислители), и обычно идентифицируются по использованию ключевого слова yield . Ниже приведен пример в Python:

 def file_lines(filename): file = open(filename) for line in file: yield line file.close() for line in file_lines('somefile'): #do some work here 

Давайте перепишем пример генератора Python на PHP. (Обратите внимание, что оба фрагмента не выполняют никакой проверки ошибок.)

 <?php function file_lines($filename) { $file = fopen($filename, 'r'); while (($line = fgets($file)) !== false) { yield $line; } fclose($file); } foreach (file_lines('somefile') as $line) { // do some work here } 

Функция генератора открывает файл, а затем выдает каждую строку файла по мере необходимости. Каждый раз, когда вызывается генератор, он продолжает с того места, где он остановился. Он не начинается снова с самого начала, поскольку его состояние было сохранено при выполнении оператора yield. После прочтения всех строк генератор просто завершается, и цикл заканчивается.

Ключи возврата

PHP-итераторы состоят из пар ключ / значение. В нашем примере было возвращено только значение, поэтому ключи были числовыми (по умолчанию ключи числовые). Если вы хотите вернуть ассоциативную пару, просто измените оператор yield на ключ, используя синтаксис массива.

 <?php function file_lines($filename) { ... yield $key => $line; ... } foreach (file_lines('somefile') as $key => $line) { // do some work here } 

Внедрение ценностей

yield не только возвращает значения; он может получать значения и извне. Это делается путем вызова метода send() объекта генератора со значением, которое вы хотите передать. Это значение может затем использоваться в вычислениях или делать другие вещи. Метод отправляет значение в генератор в результате выражения yield и возобновляет выполнение.

 <?php function nums() { for ($i = 0; $i < 5; ++$i) { // get a value from the caller $cmd = (yield $i); if ($cmd == 'stop') { return; // exit the generator } } } $gen = nums(); foreach ($gen as $v) { // we are satisfied if ($v == 3) { $gen->send('stop'); } echo "{$v}n"; } 

Выход будет:

  0
 1
 2
 3 

Экономия памяти с генераторами

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

Представьте себе функцию file() которая возвращает все строки в файле в виде массива. Выполнение простого теста для функции file() и наших демонстрационных функций file_lines (), каждая из которых использовала один и тот же случайный текстовый файл из 100 абзацев, сгенерированный с помощью Lipsum , показала, что файловая функция использует в 110 раз больше памяти, чем генератор.

 <?php // Test 1 $m = memory_get_peak_usage(); foreach (file_lines('lipsum.txt') as $l); echo memory_get_peak_usage() - $m, "n"; //Outputs 7336 // Test 2 $m = memory_get_peak_usage(); foreach (file('lipsum.txt') as $l); echo memory_get_peak_usage() - $m, "n"; // Outputs 148112 

Вывод

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