正文
PHP 在 5.5 版本中引入了「生成器(Generator)」特性,不过这个特性并没有引起人们的注意。在官方的
从 PHP 5.4.x 迁移到 PHP 5.5.x
中介绍说它能以一种简单的方式实现迭代器(Iterator)。
生成器实现通过 yield 关键字完成。生成器提供一种简单的方式实现迭代器,几乎无任何额外开销或需要通过实现迭代器接口的类这种复杂方式实现迭代。
文档提供了一个简单的实例演示这个简单的迭代器,请看下面的代码:
function xrange($start, $limit, $step = 1) {
for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
}
让我们将它与无迭代器支持的数组进行比较:
foreach xrange($start, $limit, $step = 1) {
$elements = [];
for ($i = $start; $i <= $limit; $i += $step) {
$elements[] = $i;
}
return $elements;
}
这两个版本的函数都支持
foreach
迭代获取所有元素:
foreach (xrange(1, 100) as $i) {
print $i . PHP_EOL;
}
所以除了一个更短的函数定义,我们还能获取什么呢?
yield
到底做了什么?为什么在第一个函数定义时依然可以返回数据,即使没有
return
语句?
先从返回值说起。生成器是 PHP 中的一个很特别的函数。当一个函数包含
yield
,那么这个函数即不再是一个普通函数,它永远返回一个「
Generator(生成器)
」实例。生成器实现了
Iterator
接口,这就是为何它能够进行
foreach
遍历的原因。
接下来我使用
Iterator
接口中的方法,对之前的
foreach
循环进行重写。你可以在
3v4l.org
查看结果。
$generator = xrange(1, 100);
while($generator->valid()) {
print $generator->current() . PHP_EOL;
$generator->next();
}
我们可以清楚的看到生成器是更高级的技术,现在让我们编写一个新的生成器示例来更好的理解到底在生成器内部是如何进行处理的吧。
function foobar() {
print 'foobar - start' . PHP_EOL;
for ($i = 0; $i < 5; $i++) {
print 'foobar - yielding...' . PHP_EOL;
yield $i;
print 'foobar - continued...' . PHP_EOL;
}
print 'foobar - end' . PHP_EOL;
}
$generator = foobar();
print 'Generator created' . PHP_EOL;
while ($generator->valid()) {
print "Getting current value from the generator..." . PHP_EOL;
print $generator->current() . PHP_EOL;
$generator->next();
}
Generator created
foobar - start
foobar - yielding...
Getting current value from the generator...
1
foobar - continued
foobar - yielding...
Getting current value from the generator...
2
foobar - continued
foobar - yielding...
Getting current value from the generator...
3
foobar - continued
foobar - yielding...
Getting current value from the generator...
4
foobar - continued
foobar - yielding...
Getting current value from the generator...
5
foobar - continued
foobar - end
嗯?为什么
Generator created
最先打印出来?这是因为生成器在被使用之前不会执行任何操作。在上例中就是
$generator->valid()
这句代码才开始执行生成器。我们看到生成器一直运行到了第一个
yield
时,将控制流程交还给调用者
$generator->valid()
。
$generator->next()
调用时则恢复生成器执行,到下一个
yield
再次停止运行,如此反复直到没有更多的
yield
为止。我们现在拥有了可以在任何
yield
执行暂停和回复的终端函数。这个特性允许编写客户端所需的延迟函数。
你可以创建一个从 GitHub API 读取所有用户的功能。支持分页处理,但是你可以隐藏这些细节并且仅当需要时再去获取下一页数据。你可以使用
yield
从当前页面获取每个用户数据,直到当前页所有用户获取完成,你就可以再去获取下一页数据。
class GitHubClient {
function getUsers(): Iterator {
$uri = '/users';
do {
$response = $this->get($uri);
foreach ($response->items as $user) {
yield $user;
}
$uri = $response->nextUri;
} while($uri !== null);
}
}
客户端可以迭代出所有用户或者在任何时候停止遍历。
把生成器当迭代器使用真是无聊
是的,你的想法是对的。以上我给出的所有讲解任何人都可以从 PHP 文档中获取到。但是作为迭代器这些使用,连它强大功能的一半都没用到。生成器还提供了不属于
Iterator
接口的
send()
和
throw()
功能。我们前面谈到了暂停和恢复生成器执行功能。当需要恢复生成器时,不仅可以功过
Generator::next()
方法,还可以使用
Generator::send()
和
Generator::throw()
方法。
Generator::send()
允许你指定
yield
的返回值,而
Generator::throw()
允许向
yield
抛出异常。通过这些方法我们不仅可以从生成器中获取数据,还能向生成器中发送新数据。
让我们看一个从
Cooperative multitasking using coroutines
(强烈推荐阅读本文)摘取的
Logger
日志示例。
function logger($filename) {
$fileHandle = fopen($filename, 'a');
while (true) {
fwrite($fileHandle, yield . "\n");
}
}
$logger = logger(__DIR__ . '/log');
$logger->send('Foo');
$logger->send('Bar');
yield
在这里是作为表达式使用的。当我们发送数据时,从
yield
返回数据然后作为参数传入到
fwrite()
。
讲真,这个示例在实际项目中没毛用。它仅仅用于演示
Generator::send()
的使用原理,但是仅仅能够发送数据并没有太大作用。如果有一个类和普通函数支持的话就不一样了。
使用生成器的乐趣来自于通过
yield
创建数据,然后由「生成器执行程序(generator runner)」依据这个数据来处理业务,然后再继续执行生成器。这就是「
协程(coroutines)
」和「
状态流解析器(stateful streaming parsers)
」实例。在讲解协程和状态流解析器之前,我们快速浏览一下如何在生成器中返回数据,我们还没有将接触这方面的知识。从
PHP 5.5 开始我们可以在生成器内部使用
return;
语句,但是不能返回任何值。执行
return;
语句的唯一目的是结束生成器执行。
不过从 PHP 7.0 起支持返回值。这个功能在用于迭代时可能有些奇怪,但是在其他使用场景如协程时将非常有用,例如,当我们在执行一个生成器时我们可以依据返回值处理,而无需直接对生成器进行操作。下一节我们将讲解
return
语句在协程中的使用。
异步生成器
Amp
是一款 PHP 异步编程的框架。支持异步协程功能,本质上是等待处理结果的占位符。「生成器执行程序」为
Coroutine
类。它会订阅异步生成器(yielded promise),当有执行结果可用时则继续生成器处理。如果处理失败,则会抛出异常给生成器。你可以到
amphp/amp
版本库查看实现细节。在 Amp 中的
Coroutine
本身就是一个
Promise
。如果这个协程抛出未经捕获的异常,这个协程就执行失败了。如果解析成功,那么就返回一个值。这个值看起来和普通函数的返回值并无二致,只不过它处于异步执行环境中。这就是需要生成器需要有返回值的意义,这也是为何我们将这个特性加入到 PHP 7.0 中的原因,我们会将最后执行的yield 值作为返回值,但这不是一个好的解决方案。