虽然迭代器仅需继承接口即可实现,但毕竟需要定义一整个类然后实现接口的所有方法,实在是不怎么方便。
生成器则提供了一种更简单的方式来实现简单的对象迭代,相比定义类来实现
Iterator
接口的方式,性能开销和复杂度大大降低。
生成器允许在 foreach
代码块中迭代一组数据而不需要创建任何数组。一个生成器函数,就像一个普通的有返回值的自定义函数类似,但普通函数只返回一次, 而生成器可以根据需要通过 yield
关键字返回多次,以便连续生成需要迭代返回的值。
一个最简单的例子就是使用生成器来重新实现 xrange()
函数。效果和上面我们用迭代器实现的差不多,但实现起来要简单的多。
生成器实现 xrange
函数
function xrange($start, $limit, $step = 1) {
for ($i = 0; $i < $limit; $i += $step) {
yield $i + 1 => $i;
}
}
foreach (xrange(0, 9) as $key => $val) {
printf("%d %d \n", $key, $val);
}
// 输出
// 1 0
// 2 1
// 3 2
// 4 3
// 5 4
// 6 5
// 7 6
// 8 7
// 9 8
实际上生成器生成的正是一个迭代器对象实例,该迭代器对象继承了 Iterator
接口,同时也包含了生成器对象自有的接口,具体可以参考 Generator 类的定义以及语法参考。
同时需要注意的是:
一个生成器不可以返回值,这样做会产生一个编译错误。然而 return 空是一个有效的语法并且它将会终止生成器继续执行。
yield 关键字
需要注意的是 yield
关键字,这是生成器的关键。通过上面的例子可以看出,yield
会将当前产生的值传递给 foreach
,换句话说,foreach
每一次迭代过程都会从 yield
处取一个值,直到整个遍历过程不再能执行到 yield
时遍历结束,此时生成器函数简单的退出,而调用生成器的上层代码还可以继续执行,就像一个数组已经被遍历完了。
yield
最简单的调用形式看起来像一个 return
申明,不同的是 yield
暂停当前过程的执行并返回值,而 return
是中断当前过程并返回值。暂停当前过程,意味着将处理权转交由上一级继续进行,直到上一级再次调用被暂停的过程,该过程又会从上一次暂停的位置继续执行。这像是什么呢?这很像操作系统的进程调度,多个进程在一个 CPU 核心上执行,在系统调度下每一个进程执行一段指令就被暂停,切换到下一个进程,这样外部用户看起来就像是同时在执行多个任务。
但仅仅如此还不够,yield
除了可以返回值以外,还能接收值,也就是可以在两个层级间实现 双向通信 。
来看看如何传递一个值给 yield
:
function printer()
{
while (true) {
printf("receive: %s\n", yield);
}
}
$printer = printer();
$printer->send('hello');
$printer->send('world');
// 输出
receive: hello
receive: world
根据 PHP 官方文档的描述可以知道 Generator
对象除了实现 Iterator
接口中的必要方法以外,还有一个 send
方法,这个方法就是向 yield
语句处传递一个值,同时从 yield
语句处继续执行,直至再次遇到 yield
后控制权回到外部。
既然 yield
可以在其位置中断并返回或者接收一个值,那能不能同时进行接收和返回呢?当然,这也是实现协程的根本。对上述代码做出修改:
function printer()
{
$i = 0;
while (true) {
printf("receive: %s\n", (yield ++$i));
}
}
$printer = printer();
printf("%d\n", $printer->current());
$printer->send('hello');
printf("%d\n", $printer->current());
$printer->send('world');
printf("%d\n", $printer->current());
// 输出
1
receive: hello
2
receive: world
3
这是另一个例子:
function gen() {
$ret = (yield 'yield1');
var_dump($ret);
$ret = (yield 'yield2');
var_dump($ret);
}
$gen = gen();
var_dump($gen->current()); // string(6) "yield1"
var_dump($gen->send('ret1')); // string(4) "ret1" (第一个 var_dump)
// string(6) "yield2" (继续执行到第二个 yield,吐出了返回值)
var_dump($gen->send('ret2')); // string(4) "ret2" (第二个 var_dump)
// NULL (var_dump 之后没有其他语句,所以这次 ->send() 的返回值为 null)
current
方法是迭代器 Iterator
接口必要的方法,foreach
语句每一次迭代都会通过其获取当前值,而后调用迭代器的 next
方法。在上述例子里则是手动调用了 current
方法获取值。
上述例子已经足以表示 yield 能够作为实现双向通信的工具,也就是具备了后续实现协程的基本条件。
上面的例子如果第一次接触并稍加思考,不免会疑惑为什么一个 yield
既是语句又是表达式,而且这两种情况还同时存在:
- 对于所有在生成器函数中出现的
yield
,首先它都是语句,而跟在yield
后面的任何表达式的值将作为调用生成器函数的返回值,如果yield
后面没有任何表达式(变量、常量都是表达式),那么它会返回NULL
,这一点和return
语句一致。 yield
也是表达式,它的值就是send
函数传过来的值(相当于一个特殊变量,只不过赋值是通过send
函数进行的)。只要调用 send 方法,并且生成器对象的迭代并未终结,那么当前位置的yield
就会得到send
方法传递过来的值,这和生成器函数有没有把这个值赋值给某个变量没有任何关系。
这个地方可能需要仔细品味上面两个 send()
方法的例子才能理解。但可以简单的记住:
任何时候 yield 关键词即是语句:可以为生成器函数返回值;
也是表达式:可以接收生成器对象发过来的值。
除了 send()
方法,还有一种控制生成器执行的方法是 next()
函数:
Next()
,恢复生成器函数的执行直到下一个yield
Send()
,向生成器传入一个值,恢复执行直到下一个yield