生成器 yield理解
yield
生成器是PHP 5.5.0才引入的功能
如要深入,可以看鳥哥的文摘 協程實現多工排程
生成器優點
- 生成器會對PHP應用的效能有非常大的影響
- PHP程式碼執行時節省大量的記憶體
- 比較適合計算大量的資料
那麼,這些神奇的功能究竟是如何做到的?我們先來舉個例子
概念引入
首先,來看一個簡單的PHP函式:
function createRange($number){ $data = []; for($i=0;$i<$number;$i++){ $data[] = time(); } return $data; }
這裡的程式碼也非常簡單:
我們建立一個函式, 函式內包含一個 for 迴圈,我們迴圈的把當前時間放到$data裡面, for迴圈執行完畢,把 $data 返回出去。
我們再寫一個函式,把這個函式的返回值迴圈打印出來:
$result = createRange(5);
foreach($result as $value){
sleep(1);//這裡停頓1秒,我們後續有用
echo $value.'<br />';
}
執行結果:
1540537809
1540537809
1540537809
1540537809
1540537809
猶豫數量小,sleep(1) 效果你們看不出來
思考一個問題
我們注意到,在呼叫函式 createRange 的時候給 $number 的傳值是10,一個很小的數字。假設,現在傳遞一個值10000000(1000萬)。
那麼,在函式 createRange 裡面,for迴圈就需要執行1000萬次。且有1000萬個值被放到 $data 裡面,而$data陣列在是被放在記憶體內。所以,在呼叫函式時候會佔用大量記憶體。
這裡,生成器就可以大顯身手了。
生成器的使用
建立生成器
我們直接修改程式碼,你們注意觀察:
function createRange($number){ for($i=0;$i<$number;$i++){ yield time(); } }
看下這段和剛剛很像的程式碼,我們刪除了陣列 $data ,而且也沒有返回任何內容,而是在 time() 之前使用了一個關鍵字 yield
。
使用生成器
我們再執行一下第二段程式碼:
$result = createRange(10); // 這裡呼叫上面我們建立的函式
foreach($result as $value){
sleep(1);
echo $value.'<br />';
}
執行結果:
1540538241
1540538242
1540538243
1540538244
1540538245
我們奇蹟般的發現了,輸出的值和第一次沒有使用生成器的不一樣。這裡的值(時間戳)中間間隔了1秒。
這裡的間隔一秒其實就是 sleep(1) 造成的後果。但是為什麼第一次沒有間隔?那是因為: 未使用生成器時: createRange 函式內的 for 迴圈結果被很快放到 $data 中,並且立即返回。所以, foreach 迴圈的是一個固定的陣列。 使用生成器時: createRange 的值不是一次性快速生成,而是依賴於 foreach 迴圈。 foreach 迴圈一次, for 執行一次。
到這裡,你應該對生成器有點兒頭緒。
深入理解生成器
下面我們來對於剛剛的程式碼進行剖析。
function createRange($number){
for($i=0;$i<$number;$i++){
yield time();
}
}
$result = createRange(10); // 這裡呼叫上面我們建立的函式
foreach($result as $value){
sleep(1);
echo $value.'<br />';
}
我們來還原一下程式碼執行過程。
- 首先呼叫 createRange 函式,傳入引數10,但是 for 值執行了一次然後停止了,並且告訴 foreach 第一次迴圈可以用的值。
- foreach 開始對 $result 迴圈,進來首先 sleep(1) ,然後開始使用 for 給的一個值執行輸出。
- foreach 準備第二次迴圈,開始第二次迴圈之前,它向 for 迴圈又請求了一次。
- for 迴圈於是又執行了一次,將生成的時間戳告訴 foreach .
- foreach 拿到第二個值,並且輸出。由於 foreach 中 sleep(1) ,所以, for 迴圈延遲了1秒生成當前時間
所以,整個程式碼執行中,始終只有一個記錄值參與迴圈,記憶體中也只有一條資訊。
無論開始傳入的 $number 有多大,由於並不會立即生成所有結果集,所以記憶體始終是一條迴圈的值。
繼續舉例深入理解
function gen() {
$ret = (yield 'yield1');
var_dump($ret);
$ret = (yield 'yield2');
var_dump($ret);
}
$gen = gen();
var_dump($gen->current()); // string(6) "yield1"
var_dump($gen->send('ret1')); // string(4) "ret1" (the first var_dump in gen)
// string(6) "yield2" (the var_dump of the ->send() return value)
var_dump($gen->send('ret2')); // string(4) "ret2" (again from within gen)
// NULL (the return value of ->send())
上面的程式碼首先是呼叫函式gen生成一個Generator物件,然後呼叫這個物件的current方法返回第一個值,顯然它是第一個yield語句的返回值,也就是'yield1',這個時候gen函式的執行就會被中止,接著執行var_dump($g->send('ret1'));。
呼叫$g->send('ret1'),傳入引數為字串'ret1',按照上面的說明,它會賦值給第一個yield表示式,也就是(yield 'yield1')中的yield(注意:這個時候不包括'yield1'),它的值為'ret1',然後會賦值給$ret,所以第二個輸出'ret1'就是gen函式中的第一個var_dump輸出的。此時對Generator物件的迭代會恢復繼續執行,實際上就是呼叫了一次next函式,它會執行到下一個yield語句:yield 'yield2',這個語句會返回'yield2',它會作為$g->send('ret1')的返回值,所以函式外第二個var_dump會輸出'yield2'。
最後再次呼叫send函式,這次傳入的引數為字串'ret2',跟上面一樣,Generator物件當前位置的元素是在gen函式的第二個yield上,所以’ret2'會被傳遞給第二個yield表示式,也就是作為(yield 'yield2')中的yield的值,並且會被賦值給$ret變數,然後gen函式恢復執行,它會執行gen函式中的最後一個var_dump,此時對Generator物件$g的遍歷也結束了,第二個send函式的返回值為NULL,這也是函式外的最後一個var_dump的輸出。
讀了這麼一段分析以後,你現在最大的困惑是什麼呢?
我最大的困惑是為什麼同一個yied關鍵字,它既是語句,又是表示式,而且這兩種情況是同時存在的:
對於所有在generator函式中出現的yield,首先它都是語句,而跟在yield後面的任何表示式的值將作為呼叫generator函式的返回值,如果yield後面沒有任何表示式(變數、常量都是表示式),那麼它會返回NULL,這一點跟return語句一致。
-
yield也是表示式,它的值就是send函式傳過來的值(相當於一個特殊變數,只不過賦值是通過send函式進行的)。只要呼叫send方法,並且Generator物件的迭代並未終結,那麼當前位置的
-
yield就會得到send方法傳遞過來的值,這跟generator函式有沒有把這個值賦值給某個變數沒有任何關係。
從上面兩點我們就可以看出,任何時候yield關鍵詞都即是語句——可以為generator函式返回值,也是表示式——可以接收Generator物件發過來的值。
但有點疑問就是:
在執行完gen中的var_dump之後,generator應該終止啊。但是,為什麼卻又恢復了,繼續執行下一條yield語句呢。 我猜是因為當yield作為表示式的時候,generator並沒有進行迭代。只有yield被當做了語句執行之後,generator才會終止吧。
舉例補充說明這個終止
function gen() {
for($i=1;$i<=100;$i++) {
$cmd = (yield $i);
if($cmd=='stop') {
return;
}
}
}
$gen = gen();
$i=0;
foreach($gen as $item) {
echo $item."\n";
if($i>=10) {
$gen->send('stop');
}
$i++;
}
這是個很好地例子:
- yield作為語句(類似return語句),會返回$i給呼叫者。
- yield作為表示式。獲取send函式傳遞值,賦值給$cmd。
- 實現Generator物件和generator函式的通訊。這個很重要。應該能實現很多generator的互動.
概念理解
到這裡,你應該已經大概理解什麼是生成器了。下面我們來說下生成器原理。
首先明確一個概念:生成器yield關鍵字不是返回值,他的專業術語叫產出值,只是生成一個值
那麼程式碼中 foreach 迴圈的是什麼?其實是PHP在使用生成器的時候,會返回一個 Generator 類的物件。 foreach 可以對該物件進行迭代,每一次迭代,PHP會通過 Generator 例項計算出下一次需要迭代的值。這樣 foreach 就知道下一次需要迭代的值了。
而且,在執行中 for 迴圈執行後,會立即停止。等待 foreach 下次迴圈時候再次和 for 索要下次的值的時候,迴圈才會再執行一次,然後立即再次停止。直到不滿足條件不執行結束。
迭代生成陣列: 鍵=>值
如,
function createRange($number){
for($i=0;$i<$number;$i++){
yield $i => time();
}
}
實際開發應用
- 讀取超大檔案 PHP開發很多時候都要讀取大檔案,比如csv檔案、text檔案,或者一些日誌檔案。這些檔案如果很大,比如5個G。這時,直接一次性把所有的內容讀取到記憶體中計算不太現實。
這裡生成器就可以派上用場啦。簡單看個例子:讀取text檔案
<?php
header("content-type:text/html;charset=utf-8");
function readTxt()
{
# code...
$handle = fopen("./test.txt", 'rb');
while (feof($handle)===false) {
# code...
yield fgets($handle);
}
fclose($handle);
}
foreach (readTxt() as $key => $value) {
# code...
echo $value.'<br />';
}
使用生成器讀取檔案,第一次讀取了第一行,第二次讀取了第二行,以此類推,每次被載入到記憶體中的文字只有一行,大大的減小了記憶體的使用。
這樣,即使讀取上G的文字也不用擔心,完全可以像讀取很小檔案一樣編寫程式碼。