说到 PHP 中的生成器,很多人都知道它是一种可以用来高效迭代的工具,比如:
<?php
function xrange($start, $end, $step = 1) {
for ($i = $start; $i <= $end; $i += $step) {
yield $i;
}
}
foreach (xrange(1, 1000000) as $num) {
echo $num, "\n";
}
xrange() 实现了 range() 的功能,但是 range() 返回的是一个数组,占据了对应的内存空间,而 xrange 返回的是一个生成器,在生成器内部的循环中,一次一次将数字传递出来。
当然,也可以不通过生成器来实现这个功能,而是可以通过实现 Iterator 接口。但是使用 Generator 类型,其已经实现了 Iterator 接口,因此我们可以直接使用它来进行迭代。
可以把生成器简单地看成是可中断的函数,执行过程执行到了 yield 语句,则会将控制器从该函数返回给调用者。
看看下面的例子:
$gen = call_user_func(function (){
yield 'Hello, generator stops at the first yield' . '<br/>';
yield 'Hello, generator stops at the second yield' . '<br/>';
});
var_dump($range instanceof Generator); // bool(true)
var_dump($gen instanceof Iterator); // bool(true)
$gen->rewind();
echo $gen->current(); // Hello, generator stops at the first yield
$gen->next();
echo $gen->current(); // Hello, generator stops at the second yield
$gen->next();
var_dump($gen->valid()) // false
执行 rewind() 时,生成器开始执行并停在第一个 yield 处。调用 current(),可以返回当前生成器产生的值,即第一个 yield 处的值。
调用 next() 后,生成器运行到第二个 yield 处,同样,调用 current() 将返回在第二个 yield 处返回的值。
第二次执行 next() 后,生成器从第二个 yield 处执行到整个生产器的终点,此时 valid() 将返回 false。
那么在第一个例子中,foreach() 可以看成是不断地在调用 current() 和 next(),直到 valid() 返回 false。
注意, 这里的 rewind() 是可以省略的,因为第一次调用 current() 时,生成器就会执行到第一个 yield 处,相当于执行了 rewind()。
同时,只能在生成器还没开始执行的时候调用 rewind(),否则就会报错:
( ! ) Fatal error: Uncaught Exception: Cannot rewind a generator that was already run
既然如此,rewind() 到底有什么实际用呢?看下面代码就知道了:
function getLines($file) {
$f = fopen($file, 'r');
try {
while ($line = fgets($f)) {
yield $line;
}
} finally {
fclose($f);
}
}
我们可以通过调用 rewind() 来执行 fopen(),如果打开文件失败的话,就会提前报出错误。
生成器不仅可以将数据返回给调用者,同时,也可以接收调用者发来的数据,下面的代码展示了方法:
$gen = call_user_func(function (){
$words = yield 'Hello, generator stops at the first yield' . '<br/>';
echo $words . ' from first yield' . '<br/>';
$words = yield 'Hello, generator stops at the second yield' . '<br/>';
echo $words . ' from second yield' . '<br/>';
});
echo $gen->current();
$gen->send('Hi, plz continue to run');
echo $gen->current();
$gen->send('Hi, plz continue to run');
/*
结果:
Hello, generator stops at the first yield
Hi, plz continue to run from first yield
Hello, generator stops at the second yield
Hi, plz continue to run from second yield
*/
可以看到,这里的 yield 不仅是一个语句,同时也作为一个表达式来使用,send() 会将参数值传递到生成器中当前的 yield 处,并且会让生成器运行到下一个 yield,相当于调用了 next()。yield接收到的值保存在变量$words中。