PHP 在 5.5 版本中引入了生成器 (Generator)特性, 不过这个特性并没有引起人们的注意. 在官方的 从 PHP 5.4.x 迁移到 PHP 5.5.x https://secure.php.net/manual/zh/migration55.new-features.php 中介绍说它能以一种简单的方式实现迭代器 (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(生成器) https://secure.php.net/manual/zh/class.generator.php 实例. 生成器实现了 Iterator https://secure.php.net/manual/zh/class.iterator.php 接口, 这就是为何它能够进行 foreach 遍历的原因.
接下来我使用 Iterator 接口中的方法, 对之前的 foreach 循环进行重写. 你可以在 https://3v4l.org/5uF7I 查看结果.
- $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 抛出异常. 通过这些方法我们不仅可以从生成器中获取数据, 还能向生成器中发送新数据.
- function logger($filename) {
- $fileHandle = fopen($filename, 'a');
- while (true) {
- fwrite($fileHandle, yield . "\n");
- }
- }
- $logger = logger(__DIR__ . '/log');
- $logger->send('Foo');
- $logger->send('Bar');
- Loop::run(function() {
- $uris = [
- "https://google.com/",
- "https://github.com/",
- "https://stackoverflow.com/",
- ];
- $client = new Amp\Artax\DefaultClient;
- $promises = [];
- foreach ($uris as $uri) {
- $promises[$uri] = $client->request($uri);
- }
- $responses = yield $promises;
- foreach ($responses as $uri => $response) {
- print $uri . "-" . $response->getStatus() . PHP_EOL;
- }
- });
- $parse = new Parser((function(){
- while (true) {
- $line = yield "\r\n";
- if (trim($line) === "") {
- continue;
- }
- print "New item: {$line}" . PHP_EOL;
- }
- })());
- for ($i = 0; $i <100; $i++) {
- $parser->push("bar\r");
- $parser->push("\nfoo");
- }
来源: https://juejin.im/entry/5b4c2d76f265da0f697029ad