天生器实现通过 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...
foobar - continued
foobar - yielding...
Getting current value from the generator...
foobar - continued
foobar - yielding...
Getting current value from the generator...
foobar - continued
foobar - yielding...
Getting current value from the generator...
foobar - continued
foobar - yielding...
Getting current value from the generator...
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 值作为返回值,但这不是一个好的办理方案。
Amp 可以像编写壅塞代码一样编写非壅塞代码,同时许可在同一进程中实行其它非壅塞事宜。一个利用场景是,同时对一个或多个第三方 API 并行的创建多个 HTTP 要求,但不限于此。得益于事宜循环,可以同时处理多个 I/O 处理,而不仅仅是只能处理多个 HTTP要求这类操作。
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;
}
});
但是,拥有异步功能的协程并非只能够在 yield 右侧涌现变量,还可以在它的左侧。这便是我们前面提到的解析器。
$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\公众);
}
解析器会缓存所有输入直到吸收的是 rn。这类天生器解析器并不能简化大略协议处理(如换行分隔符协议),但是对付繁芜的解析器,如在做事器解析 HTTP 要求的 Aerys。