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 值作为返回值,但这不是一个好的解决方案。
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。
相关推荐:
以上是淺談一下PHP生成器的使用方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!