摘要:執行語句的唯一目的是結束生成器執行。這就是需要生成器需要有返回值的意義,這也是為何我們將這個特性加入到中的原因,我們會將最后執行的值作為返回值,但這不是一個好的解決方案。
本文首發于 入門 PHP 生成器,轉載請注明出處。
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 . " "); } } $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 AmpArtaxDefaultClient; $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 " "; if (trim($line) === "") { continue; } print "New item: {$line}" . PHP_EOL; } })()); for ($i = 0; $i < 100; $i++) { $parser->push("bar "); $parser->push(" foo"); }
解析器會緩存所有輸入直到接收的是 rn。這類生成器解析器并不能簡化簡單協議處理(如換行分隔符協議),但是對于復雜的解析器,如在服務器解析 HTTP 請求的 Aerys。
小結生成器的功能遠超多數人的認知范圍。對于一些朋友來說可能是首次接觸生成器相關知識,一些朋友可能已經將它作為迭代器來使用,僅有很少一部分朋友使用生成器處理更多的事情。獲取你有一些很贊的想法?我很樂意進一步探討這些項目,并且希望你能從中學習到一些知識。:)
如果你需要更多資料,我推薦你閱讀 nikic 寫的 使用生成器處理多任務。
原文An Introduction to Generators in PHP
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/29054.html
摘要:例子代碼上午內存消耗量環境執行命令結果返回內存消耗量結果返回內存消耗量測試結果論述通過代碼的執行,比較除了標準函數和自定義函數之間的異同。標準函數,該函數將轉換為一個。使用場景,可查閱參考資料參考資料實際生產中的使用 什么是生成器Generators 生成器允許你在 foreach 代碼塊中寫代碼來迭代一組數據而不需要在內存中創建一個數組, 那會使你的內存達到上限,或者會占據可觀的處理...
摘要:最適合入門的初級教程四路由可以分發請求路由中還可以引入頁面我們可以在中搞定一切了但是如果把業務邏輯都寫入到路由中那路由將龐大的難以維護于是控制器就有了很明顯的存在價值把業務邏輯寫在控制器中路由只負責轉發請求到指定的控制器即可那我們開始創建控 最適合入門的Laravel初級教程(四) 路由可以分發請求; 路由中還可以引入 html 頁面;我們可以在 route/web.php 中搞定一切...
摘要:簡單點,先來實現一個擴展的。接下來我們將使用它來生成我們的擴展的基本骨架。注意不要添加任何分號。有興趣的同學可以自行研究一下靜態編譯是什么鬼在擴展目錄中執行命令。一定要在擴展的目錄執行才有效,否則將得到一個錯誤提示。 簡單點,先來實現一個PHP擴展的hello world。注意,以下所有操作都是基于linux系統(推薦centos和ubuntu, Mac系統應該類似 ),PHP5.5以...
閱讀 1061·2023-04-26 02:02
閱讀 2401·2021-09-26 10:11
閱讀 3553·2019-08-30 13:10
閱讀 3743·2019-08-29 17:12
閱讀 720·2019-08-29 14:20
閱讀 2187·2019-08-28 18:19
閱讀 2230·2019-08-26 13:52
閱讀 954·2019-08-26 13:43