生成器提供了一种更容易的方法来实现简单对象的迭代,相比较定义类实现Iterator接口的方式,性能开销和复杂性大大降低。
生成器语法
一个生成器函数看起来像一个普通函数,不同的是普通函数返回一个值,而生成器可以yield
生成许多它所需要的值。
当一个生成器被调用的时候,它返回一个可以被遍历的对象。当你遍历这个对象的时候(如通过foreach循环),PHP将会在每次需要值的时候调用生成器函数,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。当不再需要产生更多的值时,生成器函数可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经遍历完了。
生成器不能通过return返回一个值,这样做会产生一个编译错误。但是可以通过return空来终止生成器继续执行。
yield
关键字
生成器函数的核心是yield
关键字。它最简单的调用形式看起来像一个return声明,不同之处在于普通return会返回值并终止函数的执行,而yield
会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。
一个典型的yield表达式:$data = yield $key => $value;
以上yield表达式在PHP5需要加上括号
$data = (yield $key=>$value);
,否则会产生一个编译错误,PHP7则不需要关心括号问题。
这个表达式包含了两个部分:
- yield后面的
$key =>$valeue
,这里是键值对,它也可以是单个值,如下例中的$i
。这部分表达式是返回给上层调用的,也就是上层可以通过 current 方法接收到值或者在执行 send 方法的返回值; - yield本身,会收到 send 方法传入的值,这个值就是整个 yield 表达式当前的值,可以被左边的变量接收。
【示例】
<?php
// 创建生成器
function gen_one_to_three() {
for ($i = 1; $i <= 3; $i++) {
// 注意变量$i的值在不同的yield之间是保持传递的
yield $i;
}
}
// 使用生成器
$generator = gen_one_to_three();
var_dump($generator);
echo "<br />";
foreach($generator as $value) {
echo "$value <br />";
}
?>
程序输出结果:
object(Generator)#1 (0) { }
1
2
3
通过以上代码可以看出,生成器函数返回的是一个Generator对象,Generator类摘要如下:
Generator implements Iterator
{
/**
* 返回当前产生的值(yield后面表达式的值)
*/
public mixed current ( void )
/**
* 返回当前产生的键(yield 'key'=>'val';)
*/
public mixed key ( void )
/**
* 从上一个yield之后继续执行,直到下一个yield
*/
public void next ( void )
/**
* 重置迭代器(在整个循环开始前被调用(也就是生成
* Generator对象时),这样保证了我们多次遍历得到
* 的结果都是一致的)
*/
public void rewind ( void )
/**
* 向生成器中传入一个值,并从上一个yield之后继续执行
*/
public mixed send ( mixed $value )
/**
* 向生成器中抛出一个异常,并从上一个yield之后继续执行
*/
public void throw ( Exception $exception )
/**
* 检查迭代器是否被关闭(false表示已关闭)
*/
public bool valid ( void )
/**
* 序列化回调,该函数会抛出一个异常以表示生成器不能
* 被序列化。
*/
public void __wakeup ( void )
}
从以上代码中可以看出,生成器实现了Iterator
接口,因此生成器对象是可迭代的,在实现Iterator
接口的基础上还新增了throw
、send
、__wakeup
方法。
我们画个图来解释一下最开始的示例代码:
当Generator对象对foreach的时候,内部的valid,current,key,send,next方法会被依次调用。
yield的特点:
- yield只能用于函数内部,在非函数内部运用会抛出错误。
- 如果函数包含了yield关键字,那么函数执行后的返回值永远是一个Generator对象。
- 如果函数内部同时包含yield和return,该函数依然返回一个Generator对象,但在生成Generator对象时,return语句后面的代码会被忽略。
- 一旦返回的Generator对象被遍历完成,便不能调用他的rewind方法来重置。
- Generator对象不能被clone关键字克隆。
- Generator对象不能被序列化。
- Generator类实现了Iterator接口。
- 可以通过send方法给yield关键字赋一个值。
- 可以通过Generator对象的内部方法,获取到函数内部yield后面表达式的值。
多个yield语句
通常函数只能执行一次return语句,而yield却可以执行多个。
【示例】
<?php
function mygen(){
echo "开始执行yield<br/>";
yield 1;
echo "i <br/>";
yield 2;
yield 3;
yield 4;
}
$g = mygen();
foreach ($g as $k => $v){
echo "{$k} - {$v} <br/>";
}
?>
程序输出结果:
开始执行yield
0 - 1
i
1 - 2
2 - 3
3 - 4
通过以上结果可以看出,首先当遍历开始时rewind被执行,代码进入mygen()
函数开始执行,首先输出了开始执行yield
,然后遇到了第一个yield语句,此时调用Generator对象的key()
方法,获取到当前key
的值为yield出现的次序为0,然后调用current()
方法获得yield表达式后的值也就是1。然后执行valid()
因为当前为第一个yield,所以返回true,接下来执行foreach中的语句正常输出0 - 1,进入下一次迭代,此时next()
方法被执行,跳转到了第二个yield,第一个到第二个之间的代码被执行输出了i。再次进入 执行vaild()
,由于当前在第二个yield上面,所以依然是true,由于next()
执行了,所以key的值也有刚刚的0变为了1,current的值为2,正常输出 1 - 2。再次执行foreach中的语句,进入下一次迭代。继续执行next()
和vaild()
,由于此时到了第三个yield返回依然是true。key的值为2, yield为3。正常输出 2 -3,再次进入迭代,再次执行next()
,知道没有后续yield了vaild()
返回为false, 所以循环到此便终止了。
遍历Generator对象的每次迭代都只会执行前一次yield语句之后的代码,而且碰到yield语句就会返回一个值,相当于从generator函数中返回,这有点像挂起一个进程(线程)的执行(yield在很多语言中就是用于挂起进程(线程)),然后又启动它继续执行,周而复始直到进程(线程)执行中止。
send()
方法
public mixed Generator::send(mixed $value)
该函数向生成器中传入一个值,并且当做 yield 表达式的结果,然后继续执行生成器。如果当这个方法被调用时,生成器不在 yield 表达式,那么在传入值之前,它会先运行到第一个 yield 表达式。
send()方法主要用于发送数据给当前yield,即yield表达式被当作一个值被替换,且继续执行下一个yield,即next()
【示例】
<?php
function gen(){
for($i = 0; $i < 5; $i++){
echo (yield $i).$i.'<br />';
}
}
$gen = gen();
$gen->send(666);
?>
程序输出结果:
6660
首先把666代替当前yield表达式的值,然后执行next()
,即运行echo (yield i.'<br/>'
,当前yield是666,所以最终结果是:6660。
生成器委托
PHP7新增了yield from关键词,该语法开始允许从其他的 generator,Traversable 对象,或者数组通过 yield from 生成数函数 来 yield 值。yield from 的各种特性与 yield 一样都是生成数据,只是后面跟随的表达式不同。
PHP7中,通过生成器委托(yield from),可以将其他生成器、可迭代的对象、数组委托给外层生成器。外层的生成器会先顺序 yield 委托出来的值,然后继续 yield 本身中定义的值。同时yield from也能获取到生成器的返回值,和生成器的getReturn方法作用同等,需要注意这里仅仅指的是获取返回值是同等的。
【示例】
<?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 <br />";
}
?>
程序输出结果:
1
2
3
4
5
6
7
8
9
10
以上代码中生成器count_to_ten()
分别委托了数组,可迭代对象,以及生成器。count_to_ten()
在遇到委托的时候会顺序迭代委托,然后再继续本身的yield。yield from 以方便我们编写比较清晰生成器嵌套,这点可以类比于函数中的嵌套调用,当函数 A 中调用另一个函数 B,此时会等 B 执行完成并返回,方才继续执行。