我第一次见到生成器这个名词的时候,是在学习ES6语法的时候见到的。后来发现。在php中也有这个概念。不过,是在php5.5及以上版本才有的这个概念。其实不止在这些语言中有,像python,Golang等语言中。在这些语言中,对于生成器的语法稍有不同。但核心是一样的
一 、什么是生成器呢?
看下面一段代码.
<?php
function makeSque($start, $end, $step = 1) {
for($i = $start; $i <= $end; $i += $step) {
yield $i;
}
}
$gen = makeSque(1, 100);
如上所示,makeSque就是一个生成器。也许你会问了,这不就是函数吗?没错,他和函数长得一模一样。而且调用它的方式,也和函数一模一样。正因为它和函数长得很像。因此也叫做生成器函数。
然而,用来区分函数与生成器的关键就是yield关键字。所有包含yield关键字的”函数”都叫生成器。这里我指明了是”函数”里面,说明它直接在全局使用是会报错的。上面的代码就构建了一个生成一个序列的生成器。我们可以使用foreach来遍历它
foreach(makeSque(1, 1000) as $key => $value) {
echo $value . ' ';
}// echo 1 2 3 4 5 ... 1000
我们知道,任何一个类实现了Iterator接口,都可以被foreach来遍历。因此,生成器他是实现了Iterator接口的。我们可以测试一下。
var_dump($gen instanceof Iterator);//bool(true)
虽然我们可以用像调用函数的方式来创建一个生成器。但生成器它的本质是一个Genertor对象
var_dump($gen instanceof Genertor);//bool(true)
生成器神奇的地方在于,每次运行如果遇到yield关键字。那么它就会在这个地方停下来。并且将yield后面的值返回给调用者。有点像函数里面的return是不是,但它不会终止的运行,而只是在这个地方暂停。注意!!!yield后面并不限于变量,它可以是具体的值,表达式或者的函数调用,也可以什么都没有。你下一次调用它的时候,它又会从上一次停止的地方继续运行。注意了!!!是从上次yield停止的地方继续运行。是不是有一点点类似于线程的切换。一个线程被另一个线程打断的时候,它会保存它自己的”现场”,也就是保存它的上下文环境。等到再次被调度的时候,又把现场恢复,接着执行。这也是它和函数最大的不同的地方。
二、生成器的使用
既然所有生成器都实现了Iterator接口。那么它肯定拥有Iterator中的所有方法。事实上,生成器在被foreach遍历的时候,其内部是反复调用Iterator接口中的valid,current,next方法的,当然,如果你在遍历的时候加上了key,那么中间也还会调用key方法。当valid方法返回false的时候,遍历就截止了。
我们如何触发生成器从停止的继续运行呢
第一种方法:next()方法
foreach遍历就是用这种方法触发生成器往下运行的。
$gen = makeSque(1, 5);// 构造一个生成器。生成器创建的时候,会隐世调用rewind方法,因此构建生成器的同时,生成器内部的指针指向了第一个yield处。 var_dump($gen->current()) //将yield处的值返回,如果yield后面不跟任何东西,那么将返回null 此时i的值为1,因为current方法的返回值就是1。打印integer(1) $gen->next()//触发生成器继续运行,i的值加1,再次遇到yeild,又停止,此时i的值为2 var_dump($gen->current());//打印integer(2)
当然,我们需要知道生成器什么时候运行完,也就是说,生成器还能不能继续往下执行,我们可以通过valid方法来判断生成器是不是运行完成。接上面的代码
var_dump($gen->valid());//bool(true)
因为生成器还能继续往下执行,因此返回了true
第二种触发生成器往下执行的方法就是send()
在说明这个方法前,我们先看一个例子
<?php
function gen()
{
$msg1 = (yield 'foo');
var_dump($msg1);
$msg2 = (yield 'bar');
var_dump($msg2);
}
$gen = gen();
var_dump($gen->current());// String(foo)
var_dump($gen->send('hello'));//String(hello) String(bar)
var_dump($gen->send('world'));//String(world) null
看了上面的输出,你可能不明白为什么是这样输出。我来详细的说明一下。
上面说了,构造生成器的时候会隐式的调用rewind方法。因此第一次调用current方法的时候,返回第一个yield处后面的值。此时yield是被当成php的语句来看待的,类似于php中的echo。接着我们调用send方法,并往里面传了一个值。那么接受这个值的是那个呢? 答案就是yield,此时的yield被当成了表达式来处理,它接收了从send传过来的值,并赋值给了msg1变量。接着msg1在生成器里面被打印,再往下生成器又遇到了yield关键字,暂停执行,并返回yield后面的东西。(send方法能接收返回值,这也是与next方法的一个不同之处)因此我们程序运行到var_dump($gen->send(‘hello’))这里的时候会是上面的输出情况。那么再次调用send的方法的时候,同理,我就不解释了。
上面的例子里,我们看到了我们使用send方法,可以往生成器里面传递值,生成器也可以向外面返回值。这个过程是不是类似于通信。是的!实现生成器的通信就是使用send方法来完成的。
还是上面的例子,我们换一种方式来调用生成器
$gen = gen(); var_dump($gen->current());// String(foo) $gen->next(); var_dump($gen->current());// null String(bar) var_dump($gen->send('hello'));//String(hello) null var_dump($gen->send('world'));//null
和上面例子不同的是,我们使用next的方法推进生成器往下执行了一次,再调用send方法。从上面的输出我们可以看出,send传给生成器的值,是由上一次暂停处的yield来接受,而不是按照yield出现的顺序来接收的。
好了,说了这么多。其实我并没有提及生成器有什么好处。只是概述了一下生成器的概念和生成器的使用。php生成器优点之一是在生成序列的时候比普通方法比如range()更加节省内存。对于生成大的序列的时候,优点还是比较明显的。然而,它的强大之处是在于能用它来实现协程。在我的下一篇文章里我会重点来讨论用生成器来实现协程。