php生成器

来源:互联网 发布:中国洗脑 知乎 编辑:程序博客网 时间:2024/05/16 05:09

我第一次见到生成器这个名词的时候,是在学习ES6语法的时候见到的。后来发现。在php中也有这个概念。不过,是在php5.5及以上版本才有的这个概念。其实不止在这些语言中有,像python,Golang等语言中。在这些语言中,对于生成器的语法稍有不同。但核心是一样的

一 、什么是生成器呢?

看下面一段代码.

<?phpfunction 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的值为2var_dump($gen->current());//打印integer(2)

当然,我们需要知道生成器什么时候运行完,也就是说,生成器还能不能继续往下执行,我们可以通过valid方法来判断生成器是不是运行完成。接上面的代码

var_dump($gen->valid());//bool(true)

因为生成器还能继续往下执行,因此返回了true

第二种触发生成器往下执行的方法就是send()

在说明这个方法前,我们先看一个例子

<?phpfunction 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) nullvar_dump($gen->send('world'));//null

和上面例子不同的是,我们使用next的方法推进生成器往下执行了一次,再调用send方法。从上面的输出我们可以看出,send传给生成器的值,是由上一次暂停处的yield来接受,而不是按照yield出现的顺序来接收的。

好了,说了这么多。其实我并没有提及生成器有什么好处。只是概述了一下生成器的概念和生成器的使用。php生成器优点之一是在生成序列的时候比普通方法比如range()更加节省内存。对于生成大的序列的时候,优点还是比较明显的。然而,它的强大之处是在于能用它来实现协程。在我的下一篇文章里我会重点来讨论用生成器来实现协程。