本文首發於 入門 PHP 生成器,轉載請註明出處。
PHP 在 5.5 版本中引入了「生成器(Generator)」特性,不過這個特性並無引發人們的注意。在官方的 從 PHP 5.4.x 遷移到 PHP 5.5.x 中介紹說它能以一種簡單的方式實現迭代器(Iterator)。php
生成器實現經過 yield 關鍵字完成。生成器提供一種簡單的方式實現迭代器,幾乎無任何額外開銷或須要經過實現迭代器接口的類這種複雜方式實現迭代。
文檔提供了一個簡單的實例演示這個簡單的迭代器,請看下面的代碼:html
function xrange($start, $limit, $step = 1) { for ($i = $start; $i <= $limit; $i += $step) { yield $i; } }
讓咱們將它與無迭代器支持的數組進行比較:git
foreach xrange($start, $limit, $step = 1) { $elements = []; for ($i = $start; $i <= $limit; $i += $step) { $elements[] = $i; } return $elements; }
這兩個版本的函數都支持 foreach 迭代獲取全部元素:github
foreach (xrange(1, 100) as $i) { print $i . PHP_EOL; }
因此除了一個更短的函數定義,咱們還能獲取什麼呢?yield 到底作了什麼?爲何在第一個函數定義時依然能夠返回數據,即便沒有 return 語句?shell
先從返回值提及。生成器是 PHP 中的一個很特別的函數。當一個函數包含 yield,那麼這個函數即再也不是一個普通函數,它永遠返回一個「Generator(生成器)」實例。生成器實現了 Iterator 接口,這就是爲什麼它可以進行 foreach 遍歷的緣由。編程
接下來我使用 Iterator 接口中的方法,對以前的 foreach 循環進行重寫。你能夠在 3v4l.org 查看結果。數組
$generator = xrange(1, 100); while($generator->valid()) { print $generator->current() . PHP_EOL; $generator->next(); }
咱們能夠清楚的看到生成器是更高級的技術,如今讓咱們編寫一個新的生成器示例來更好的理解到底在生成器內部是如何進行處理的吧。promise
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。
生成器的功能遠超多數人的認知範圍。對於一些朋友來講多是首次接觸生成器相關知識,一些朋友可能已經將它做爲迭代器來使用,僅有不多一部分朋友使用生成器處理更多的事情。獲取你有一些很讚的想法?我很樂意進一步探討這些項目,而且但願你能從中學習到一些知識。:)
若是你須要更多資料,我推薦你閱讀 nikic 寫的 使用生成器處理多任務。