PHP从5.5.0版本开始支持生成器(Generator),根据PHP官方文档的说法:生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大下降。 php
因此生成器首先是一个迭代器(Iterator),也就是说它可使用foreach
进行遍历。生成器就相似一个返回数组的函数,它能够接收参数,并被调用。html
咱们以range()
函数为例,把它实现为生成器:git
<?php function xrange($start, $end, $step = 1) { for ($i = $start; $i <= $end; $i += $step) { yield $i; } } echo 'results from range():'; foreach (range(1, 10, 3) as $v) { echo "$v "; } echo PHP_EOL . 'results from xrange():'; foreach (xrange(1, 10, 3) as $v) { echo "$v "; }
结果看起来是同样的:github
results from range():1 4 7 10 results from xrange():1 4 7 10
能够看到,xrange()
使用yield
关键字,而不是return
。使用yield
关键字后,调用函数时就会返回一个生成器(Generator)的对象(Generator是一个内部类,不能直接实例化),这个对象实现了Iterator接口,因此正如前面说过,生成器是迭代器,咱们能够经过如下代码验证下:数组
<?php // bool(true) var_dump(xrange() instanceof Iterator);
跟普通函数只返回一次值不一样的是, 生成器能够根据须要yield屡次,以便生成须要迭代的值。 普通函数return后,函数会被从栈中移除,停止执行,可是yield会保存生成器的状态,当被再次调用时,迭代器会从上次yield的地方恢复调用状态继续执行。看下下面代码的执行结果:bash
<?php function xrange($start, $end, $step = 1) { echo "The generator has started" . PHP_EOL; for ($i = $start; $i <= $end; $i += $step) { yield $i; echo "Yielded $i" . PHP_EOL; } echo "The generator has ended" . PHP_EOL; } foreach (xrange(1, 10, 3) as $v) { echo "return $v" . PHP_EOL; }
The generator has started return 1 Yielded 1 return 4 Yielded 4 return 7 Yielded 7 return 10 Yielded 10 The generator has ended
能够看到,每次迭代,在yield后,代码不会继续执行,而是先执行调用者的代码,而后在下一次迭代,迭代器的代码继续执行,一直到没有yield能够执行为止。函数
前面说过,函数里使用yield
关键字后,在被调用时会返回一个生成器对象,因此生成器函数的核心是yield关键字。它的调用形式看起来像一个return申明,不一样之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用今生成器的代码而且只是暂停执行生成器函数。 oop
一个生成器函数不能够经过return返回值(很显而易见,由于生成器函数被调用后返回的是一个生成器对象), 在PHP 5.6版本及以前,若是使用return返回一个值的话,会产生一个编译错误:性能
PHP Fatal error: Generators cannot return values using "return" in /path/to/php_code.php on line x
在PHP 7中,可使用getReturn()
获得return的返回值:测试
<?php function gen_return() { for ($i = 0; $i < 3; $i++) { yield $i; } return 1; } $gen = gen_return(); foreach($gen as $v); echo $gen->getReturn(); // 1
不过有个前提,就是生成器已经完成了迭代,不然会报如下错误:
PHP Fatal error: Uncaught Exception: Cannot get return value of a generator that hasn't returned in /path/to/php_code.php:x
另外,return空不管是在PHP 7仍是以前支持生成器的PHP版本都是一个有效的语法,它会终止生成器继续执行。
若是yield后面没有跟任何的参数,则会返回NULL值:
<?php function gen_nulls() { for ($i = 0; $i < 3; $i++) { yield; } } var_dump(iterator_to_array(gen_nulls()));
输出:
array(3) { [0]=> NULL [1]=> NULL [2]=> NULL }
PHP的数组支持关联键值对数组,生成器其实也支持生成键值对:
<?php function gen_key_values() { for ($i = 0; $i < 3; $i++) { yield 'key' . $i => $i; } } var_dump(iterator_to_array(gen_key_values()));
输出:
array(3) { ["key0"]=> int(0) ["key1"]=> int(1) ["key2"]=> int(2) }
除了生成值,生成器还能从外面接收值。经过生成器对象的send()
方法,咱们能够从外面传递值到生成器里。这个值会做为yield表达式的结果,咱们能够利用这个值来作一些计算或者其余事情,例如根据值来停止生成器的执行:
<?php function nums() { for ($i = 0; $i < 5; ++$i) { // 从caller获取值 $cmd = (yield $i); if ($cmd === 'stop') { return; // 退出生成器 } } } $gen = nums(); foreach ($gen as $v) { if ($v === 3) { $gen->send('stop'); } echo $v . PHP_EOL; }
输出结果:
0 1 2 3
send()
方法的返回值是下一个yield的值,若是没有,则返回NULL。
须要注意的是, 若是在一个表达式上下文(例如上面的状况,在一个赋值表达式的右侧)中使用yield,必须使用圆括号把yield申明包围起来。 例如:
$data = (yield $value);
下面的代码在PHP5中会产生一个编译错误:
$data = yield $value;
在PHP 7里,使用yield from
表达式容许你在生成器里经过其余生成器、Traversable对象或者数组产生值。这种方式叫作生成器委托。下面的例子来自官方文档:
<?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
生成器也是迭代器,那为何不直接使用迭代器呢?其实文章刚开始就说到了:生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大下降。
要使用迭代器,必需要实现Iterator接口里的全部方法,这无疑大大增长了使用成本,具体能够看看官方文档里的例子:Comparing generators with Iterator objects。
除了复杂度,另一个使用生成器的缘由就是使用生成器能够大大减小内存的使用。以文章最开始的例子为例,标准的 range()
函数须要在内存中生成一个数组包含每个在它范围内的值,而后返回该数组,这样就会产生多个很大的数组。 好比,调用 range(0, 1000000) 将致使内存占用超过 100 MB。而咱们实现的xrange()
生成器, 只须要足够的内存来建立 生成器对象并在内部跟踪生成器的当前状态,这样只须要不到1K字节的内存。
<?php function xrange($start, $end, $step = 1) { for ($i = $start; $i <= $end; $i += $step) { yield $i; } } echo 'Test for range():' . PHP_EOL; $startTime = microtime(true); $m = memory_get_peak_usage(); foreach (range(1, 1000000) as $v); $endTime = microtime(true); echo 'time:' . bcsub($endTime, $startTime, 4) . PHP_EOL; echo 'memory (byte):' . (memory_get_peak_usage() - $m); echo PHP_EOL; echo 'Test for xrange():' . PHP_EOL; $startTime = microtime(true); $m = memory_get_peak_usage(true); foreach (xrange(1, 1000000) as $v); $endTime = microtime(true); echo 'time:' . bcsub($endTime, $startTime, 4) . PHP_EOL; echo 'memory (byte):' . (memory_get_peak_usage(true) - $m);
测试结果:
Test for range(): time:0.2319 memory (byte):144376424 Test for xrange(): time:0.1382 memory (byte):0
能够看到,在内存占用上,xrange()
远远低于range()
,甚至在速度上也占优。在诸如读取文件之类的场景,使用生成器也能够大大减小内存的占用:
<?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 something }
PHP的生成器特性使得在PHP中实现协程成为了可能,下面是一篇使用协程实现多任务调度的文章,虽然是12年的文章,可是仍然颇有参考意义:
http://nikic.github.io/2012/1...