PHP生成器是一种特殊的迭代器,可以在遍历大型数据集时节省内存。它允许你在需要时生成数据,而不是一次性加载所有数据到内存中。生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。
一、生成器概述
生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。
一个简单的例子就是使用生成器来重新实现 range() 函数。 标准的 range() 函数需要在内存中生成一个数组包含每一个在它范围内的值,然后返回该数组, 结果就是会产生多个很大的数组。 比如,调用 range(0, 1000000) 将导致内存占用超过 100 MB。
做为一种替代方法, 我们可以实现一个 xrange() 生成器, 只需要足够的内存来创建 Iterator 对象并在内部跟踪生成器的当前状态,这样只需要不到1K字节的内存。
将 range() 实现为生成器:
<?php function xrange($start, $limit, $step = 1) { if ($start <= $limit) { if ($step <= 0) { throw new LogicException('Step must be positive'); } for ($i = $start; $i <= $limit; $i += $step) { yield $i; } } else { if ($step >= 0) { throw new LogicException('Step must be negative'); } for ($i = $start; $i >= $limit; $i += $step) { yield $i; } } } /* * 注意下面range()和xrange()输出的结果是一样的。 */ echo 'Single digit odd numbers from range(): '; foreach (range(1, 9, 2) as $number) { echo "$number "; } echo "\n"; echo 'Single digit odd numbers from xrange(): '; foreach (xrange(1, 9, 2) as $number) { echo "$number "; } ?>
以上示例会输出:
Single digit odd numbers from range(): 1 3 5 7 9 Single digit odd numbers from xrange(): 1 3 5 7 9
二、Generator对象
调用生成器函数时会返回一个内部的 Generator 类的对象。 该对象实现了 Iterator 接口,基本上和仅向前的迭代器一样, 它提供的方法可以操控生成器的状态,包括发送值、返回值。
三、生成器语法
生成器函数看起来像普通函数——不同的是普通函数返回一个值,而生成器可以 yield 生成多个想要的值。 任何包含 yield 的函数都是一个生成器函数。
当一个生成器被调用的时候,它返回一个可以被遍历的对象.当你遍历这个对象的时候(例如通过一个foreach循环),PHP 将会在每次需要值的时候调用对象的遍历方法,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。
一旦不再需要产生更多的值,生成器可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。
注意:生成器能够返回多个值,通过 Generator::getReturn() 可以获取到。
1、yield关键字
生成器函数的核心是yield关键字。它最简单的调用形式看起来像一个return申明,不同之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。
一个简单的生成值的例子:
<?php function gen_one_to_three() { for ($i = 1; $i <= 3; $i++) { //注意变量$i的值在不同的yield之间是保持传递的。 yield $i; } } $generator = gen_one_to_three(); foreach ($generator as $value) { echo "$value\n"; } ?>
以上示例会输出:
1 2 3
注意:在内部会为生成的值配对连续的整型索引,就像一个非关联的数组。
2、指定键名来生成值
PHP的数组支持关联键值对数组,生成器也一样支持。所以除了生成简单的值,你也可以在生成值的时候指定键名。
如下所示,生成一个键值对与定义一个关联数组十分相似。
<?php /* * 下面每一行是用分号分割的字段组合,第一个字段将被用作键名。 */ $input = <<<'EOF' 1;PHP;Likes dollar signs 2;Python;Likes whitespace 3;Ruby;Likes blocks EOF; function input_parser($input) { foreach (explode("\n", $input) as $line) { $fields = explode(';', $line); $id = array_shift($fields); yield $id => $fields; } } foreach (input_parser($input) as $id => $fields) { echo "$id:\n"; echo " $fields[0]\n"; echo " $fields[1]\n"; } ?>
以上示例会输出:
1: PHP Likes dollar signs 2: Python Likes whitespace 3: Ruby Likes blocks
3、生成null值
Yield 可以在没有参数传入的情况下被调用来生成一个 null 值并配对一个自动的键名。
<?php function gen_three_nulls() { foreach (range(1, 3) as $i) { yield; } } var_dump(iterator_to_array(gen_three_nulls())); ?>
以上示例会输出:
array(3) { [0]=> NULL [1]=> NULL [2]=> NULL }
4、使用引用来生成值
生成函数可以像使用值一样来使用引用生成。这个和从函数返回一个引用一样:通过在函数名前面加一个引用符号。
<?php function &gen_reference() { $value = 3; while ($value > 0) { yield $value; } } /* * 我们可以在循环中修改 $number 的值,而生成器是使用的引用值来生成,所以 gen_reference() 内部的 $value 值也会跟着变化。 */ foreach (gen_reference() as &$number) { echo (--$number).'... '; } ?>
以上示例会输出:
2... 1... 0...
5、yield from生成器委托
生成器委托允许使用 yield from 关键字从另外一个生成器、 Traversable 对象、array 通过生成值。 外部生成器将从内部生成器、object、array 中生成所有的值,直到它们不再有效, 之后将在外部生成器中继续执行。
如果生成器与 yield from 一起使用,那么 yield from 表达式将返回内部生成器返回的任何值。
6、存储到array
(例如使用 iterator_to_array())yield from 不能重置 key。它保留 Traversable 对象或者 array 返回的 key。因此,某些值可能会与其他的 yield 或者 yield from 共享公共的 key,因此,在插入数组时将会用这个 key 覆盖以前的值。
一个非常重要的常见情况是 iterator_to_array() 默认返回带 key 的 array , 这可能会造成无法预料的结果。 iterator_to_array() 还有第二个参数 preserve_keys ,可以设置为 false 来收集 Generator 返回的不带 key 的所有值。
使用 iterator_to_array() 的 yield from:
<?php function inner() { yield 1; // key 0 yield 2; // key 1 yield 3; // key 2 } function gen() { yield 0; // key 0 yield from inner(); // keys 0-2 yield 4; // key 1 } // 传递 false 作为第二个参数获得数组 [0, 1, 2, 3, 4] var_dump(iterator_to_array(gen())); ?>
以上示例会输出:
array(3) { [0]=> int(1) [1]=> int(4) [2]=> int(3) }
yield from 的基本用法:
<?php function count_to_ten() { yield 1; yield 2; yield from [3, 4]; yield from new ArrayIterator([5, 6]); yield from seven_eight(); yield 9; yield 10; } function seven_eight() { yield 7; yield from eight(); } function eight() { yield 8; } foreach (count_to_ten() as $num) { echo "$num "; } ?>
以上示例会输出:
1 2 3 4 5 6 7 8 9 10
yield from 并返回多个值:
<?php function count_to_ten() { yield 1; yield 2; yield from [3, 4]; yield from new ArrayIterator([5, 6]); yield from seven_eight(); return yield from nine_ten(); } function seven_eight() { yield 7; yield from eight(); } function eight() { yield 8; } function nine_ten() { yield 9; return 10; } $gen = count_to_ten(); foreach ($gen as $num) { echo "$num "; } echo $gen->getReturn(); ?>
以上示例会输出:
1 2 3 4 5 6 7 8 9 10
四、与Iterator对象比较
生成器最主要的优点是简洁。和实现一个 Iterator 类相较而言, 同样的功能,用生成器可以编写更少的代码,可读性也更强。 举例,下面的类和函数是相等的:
<?php function getLinesFromFile($fileName) { if (!$fileHandle = fopen($fileName, 'r')) { return; } while (false !== $line = fgets($fileHandle)) { yield $line; } fclose($fileHandle); } // 比较下... class LineIterator implements Iterator { protected $fileHandle; protected $line; protected $i; public function __construct($fileName) { if (!$this->fileHandle = fopen($fileName, 'r')) { throw new RuntimeException('Couldn\'t open file "' . $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); } } ?>
不过,这也付出了灵活性的代价: 生成器是一个只能向前的迭代器,一旦开始遍历就无法后退。 意思也就是说,同样的生成器无法遍历多次:要么再次调用生成器函数,重新生成后再遍历。
参见:《PHP遍历对象》