寫在前面
這篇文章,要和大家探討的是 PHP yield 在 生成器用法,不帶 foreach,for, while 循環的那種。就討論 yield 將一個函數變成為生成器的用法。
關於yield特性,是在開發PHP5時被提上日程,PHP5.5版本正式加入。
關於yield的使用,我看到大部分文章都停留在,使用yield如何在foreach中傳出數據,今天想給大家講講 生成器 所有語法。
三部曲
- yield 語法探究
- yield from 語法探究
- yield 實戰“多線程”編碼
官網講解
生成器允許你在 foreach 代碼塊中寫代碼來迭代一組數據而不需要在內存中創建一個數組, 那會使你的內存達到上限,或者會佔據可觀的處理時間。相反,你可以寫一個生成器函數,就像一個普通的自定義函數一樣, 和普通函數只返回一次不同的是, 生成器可以根據需要 yield 多次,以便生成需要迭代的值。
看了下官網對他講解:php.net 生成器語法 . 每個字都認識,但似乎還是體會到它講的內涵。官網我們主要看兩部分內容:
yield的語法。- 使用例子。
先説語法, yield 的左邊是一個賦值語句,右邊可以是值(也可是表達式) 。而yield 會先執行右邊的表達式,並把值$value送到生成器外面。當生成器收到值後,會執行yield左邊的語句,賦值給$data.
<?php
$data = (yield $express);
語法講完了,估計大家還是有些懵,那就看看官網下面代碼例子吧,我看裏面例子參差不齊。
通過例子來了解它
不論是學 人類語言,計算機語言,都是模仿開始
對於一個用人類語言來描述,都不那麼明晰時,所以那就通過例子告訴你它能做什麼,不能做什麼。
相關代碼,我放到gitee了,希望你能複製到你本地運行下,親自運行感受下,有助於了理解接下來的內容。
git clone https://gitee.com/xupaul/PHP-...
怎樣才能產生 Generator
先定義一個函數,在函數內 寫個 yield 關鍵詞,將這個函數調用賦值給一個變量。一個生成器就產生了。
由於例子代碼很多,我把例子放到 gitee 了。同時以下的文章中提到"例子"就是在 gitee 中的代碼。
代碼 /php-yield-test/yieldFunctions.php 是生成器按照不同語法組合定義了多個生成器。
測試代碼 /php-yield-test/whatIsGenerator.php,用來檢查哪些函數能構成生成器,哪些不能。運行結果如下
- 函數內必須有
yield關鍵詞,函數可以是全局函數,或者類的方法。 - 哪怕
yield肯定不會被執行,也會產生生成器。見:yield_func4 - 光禿禿 的
yield關鍵詞就行(不向外送出,不處理外面的輸入)。見: yield_func2 - 函數內使用 生成器 並不能讓自己也成為生成器,見:yield_func5
- eval函數中直接運行
yield會報錯, 見:yield_func11
是的,函數內有沒有foreach,while,for 語句都不是關鍵,關鍵是 yield. 生成器的類型判斷用 $gen instanceof Generator
生成器的函數
Generator 對象是從 generators返回的.
Generator 對象不能通過 new 實例化.
- Generator::current — 返回當前產生的值
- Generator::key — 返回當前產生的鍵
- Generator::next — 生成器繼續執行
- Generator::rewind — 重置迭代器
- Generator::send — 向生成器中傳入一個值
- Generator::throw — 向生成器中拋入一個異常
- Generator::valid — 檢查迭代器是否被關閉
- Generator::__wakeup — 序列化回調
- Gengerator::getReturn - Get the return value of a generator
摘自 php.net generator看着以上方法,是不想起了
Iterator, 他們的確很像。同時注意,官網zh語言版本的文檔沒有索引方法getReturn,訪問也是404。文檔以en版為準,ch做參考。
以上就是生成器所有的方法,我們一個個來看。
測試方法代碼 /php-yield-test/generatorMothod.php, 這裏面對每個方法都有使用舉例,運行結果如下。
好上面的運行結果,乾巴巴的,不詳細。我以下面的程序畫個流程圖:
<?php
function yield_func()
{
echo 'run yield_func' . PHP_EOL;
$get = (yield 12);
echo $get . PHP_EOL;
$get2 = (yield 55);
echo $get2 . PHP_EOL;
return 'a';
}
$gen = yield_func();
$re = $gen->current();
echo 'get re: ' . $re . PHP_EOL;
$gen->send('cc');
$re2 = $gen->current();
echo 'get re2: ' . $re2 . PHP_EOL;
$gen->send('hello');
$re3 = $gen->getReturn();
echo 'get return: ' . $re3 . PHP_EOL;
圖中,看到yield_func()函數作為生成器後,被yield 分成了一塊一塊的代碼段,沒執行一段後,就跳出,等待外部程序的調度,這就好比多線程一樣,執行時,隨時都有可能被打斷,讓出CPU,不過協程是手動調用yield讓出,程序運行順序是可預期的。調用了current()開始執行,當運行到yield代碼,讓出cpu時,調用send(),又能讓生成器繼續運行了。最後getReturn()獲取生成器的返回值。
好的,先有個大致概念,現在我們仔仔細細瞭解下各個函數。
Generator::current
- 返回當前產生的值
<?php
function yield_func()
{
yield 12;
return 'a';
}
$gen = yield_func();
$re = $gen->current();
echo 'current return : ' . $re;
輸出:
current return : 12
看到 php-yield-test/generatorMothod.php 代碼。
通過第一個代碼事例,可得,對一個generator調用current方法,才算真正開始執行。執行到yield為止。如果不能命中yield,則執行到函數結束。
非generoator會立馬執行並得到結果,而非一個生成器對象。
通過例子2,調用current一次,兩次呢,第一次可以看到代碼執行日誌,第二次,只是把上一次的結果返回給我們而已,並不是讓該生成器重新執行。
通過例子1,調用該函數還會獲取到返回值,返回的內容就是 yield 表達式左邊的內容。如果表達式無內容,則是NULL.
Generator::send
- 向生成器中傳入一個值
<?php
function yield_func()
{
$data = yield 12;
echo 'get yield data: ' . $data;
return 'a';
}
$gen = yield_func();
$re = $gen->current();
$gen->send(32);
輸出:
get yield data: 32
例子3,是一個current,send的常規調用。調用current代碼運行yield等到用户send輸入參數。接收到輸入後,繼續運行。current能夠接收到yield彈出的值,send返回值為空。
例子4,直接調用send,相當於調用current,send。不過current的返回值,並不會通過send傳給用户。
也就是説:跳過current,直接調用send,會丟失yield的彈出值。
轉載著名出處 sifou
Generator::next
- 讓生成器繼續執行
<?php
function yield_func()
{
echo 'run to code line: ' . __LINE__ . PHP_EOL;
yield;
echo 'run to code line: ' . __LINE__ . PHP_EOL;
return $result;
}
$gen = yield_func();
$gen->current();
echo 'current called' . PHP_EOL;
$gen->next();
輸出:
run to code line: 4
current called
run to code line: 6
例子5,這是一個較為常規的調用,調用current代碼運行yield等到用户輸入,這是調用next跳過,讓代碼繼續運行。
例子6,直接調用next,相當於調用current,next。而且通過最後打印$result, 我們發現怎麼有點像在調用 $gen->send(NULL);。
Generator::rewind
- 重置迭代器
<?php
function yield_func()
{
echo 'run to code line: ' . __LINE__ . PHP_EOL;
$result = yield 12;
echo 'run to code line: ' . __LINE__ . PHP_EOL;
}
$gen = yield_func();
echo 'call yield_func rewind ' . PHP_EOL;
$gen->rewind();
輸出:
call yield_func rewind
run to code line: 4
例子7,8 中,發現調用該方法,會導致隱式調用current。
例子9 中,發現在執行過一個yield代碼段後,再次調用該方法,會導致報錯(哪怕該 生成器已結束)。
Generator::throw
- 向生成器中拋入一個異常
<?php
function yield_func()
{
try {
$re = yield 'exception';
} catch (Exception $e) {
echo 'catched exception msg: ' .$e->getMessage();
}
}
$gen = yield_func();
$gen->throw(new \Exception('new yield exception'));
輸出:
catched exception msg: new yield exception
通過以上簡單的例子可得,throw 就是讓yield這行代碼產生異常,讓外面的try catch 捕獲我們生成的那個異常。
例子11中,構造生成器,並調用current方法,運行到yield處,再調用throw,就能捕獲到異常。
例子12中,當調用send方法,跳過函數內yield代碼時,再調用throw傳入異常,就沒法捕獲了。
Generator::valid
- 檢查迭代器是否被關閉
<?php
function yield_func()
{
yield 12;
return 'a';
}
$gen = yield_func();
$gen->send(1);
$check = $gen->valid();
echo 'the generator valid ? ' . intval($check);
輸出:
the generator valid ? 0
例子12中,發現current被隱式調用。
例子13中,可得,當生成器運行到yield代碼段時,用valid函數檢查,都會返回true。
所以,別問我是否已運行,問就是運行。該方法用來獲取是否關閉狀態,不是 是否運行狀態!運行到底,運行到return就是 關閉狀態。
Generator::key
- 返回當前產生的鍵
<?php
function yield_func()
{
yield 1 => 'abc';
}
$gen = yield_func();
echo 'value is :' . $gen->current() . PHP_EOL;
echo 'key is: ' . $gen->key() . PHP_EOL;
輸出:
value is :abc
key is: 1
從以上例子中,可得yield可顯示設置返回的key.
例子15 中,發現key的分發規律和PHP數組鍵值發放策略是差不多的,默認從0開始,未指定則是以上一個數字key+1作為當前的key.
例子16 中,我們又發現current被隱式調用。
Generator::__wakeup
- Generator::__wakeup — 序列化回調
<?php
function yield_func()
{
yield 1 => 'abc';
}
$gen = yield_func();
try {
$ser = serialize($gen);
} catch (\Exception $e) {
print_r($e->getMessage());
}
輸出:
Serialization of 'Generator' is not allowed
這是一個魔術方法,見 PHP 魔術方法,也就是説 生成器 不能被序列化成一個字符串。
例子17就不用説了,看下例子18,看樣子序列化成功了。也就是説一個生成器做為一個方法可以被序列化,當函數變成生成器時,就不能被序列化了。
Generator::getReturn
<?php
function yield_func()
{
yield 1 => 'abc';
return 32;
}
$gen = yield_func();
$gen->send(0);
echo 'call yield_func return, and get: ' . $gen->getReturn();
輸出:
call yield_func return, and get: 32
該函數就是獲取生成器最後的返回值。如果沒有return語句,或者沒有執行到return語句,調用該函數得到的就是NULL。
例子19 可得,getReturn 能夠獲取到生成器最後的返回值。
例子19、20 可得,當生成器沒有執行到return語句,或者沒有執行到最後時,調用getReturn是會導致報錯。
綜上所述
到這裏,我們就發現rewind,next 和 __wakeup 這兩個函數感覺沒啥叼用呢,為啥還存在呢,因為Generator繼承Iterator,自然就有了rewind, next方法,PHP 雖然支持方法覆蓋,但子類的訪問修飾符 不能縮緊,所以Generator只能重寫這兩個方法。 __wakeup 繼承自 stdClass。
狀態轉換
看圖:
畫了兩個狀態轉換圖,上面的要細緻,繁複一點。下面的精簡版,便於快速理解。
總結
以上就是關於 PHP 生成器所有內容,希望你能學會掌握這門強大的語法,下一講,我們手把手一起來做一個任務調度器,實戰一下。
有問題歡迎提問,謝謝大家!