yield:
對於yield方法和Generator的send同時使用時的執行順序一直搞不清,今天看到這篇
加上測試,終於搞清了。
總結一下上文中的結論:
- Generator提供了一種方便的實現簡單的Iterator(迭代器)的方式,使用Generator實現Iterator不需要創建一個類來繼承Iterator接口。
- Generator實現了Iterator中的5個方法,還提供了三個新方法,其中__wakeup是一個魔術方法,用於序列化,Generator實現這個方法是為了防止序列化。
- Generator 對象不能通過 new 實例化。
- yield關鍵字只能在函數中使用(你可以嘗試下在函數外使用,看看會發生什么),而且使用了yield關鍵字的函數都會返回一個Generator對象。
- yield語句有點像return語句,代碼執行到yield語句,generator函數的執行就會終止,並且會返回yield語句中的表達式的值給 Generator對象,這跟return語句一樣,不同的是,這返回值只是作為遍歷Generator對象的當前元素,而不能賦值給其他變量。
- 當對Generator對象繼續迭代,generator函數中的yield后面的代碼會繼續執行,直到generator函數中的yield語句全部執 行完畢,或者是碰到generator函數中的空return語句(返回null的return語句),在generator函數中使用帶有非null返 回值的return語句會報編譯錯誤。
- 如果yield后面沒有任何表達式(變量、常量都是表達式),那么它會返回NULL,這一點跟return語句一致。
- yield也可以返回鍵值對的形式。
- 在send之前, 當$gen迭代器被創建的時候一個rewind()方法已經被隱式調用,這樣rewind的執行將會導致第一個yield被執行, 並且忽略了他的返回值.真正當我們調用yield的時候, 我們得到的是第二個yield的值! 導致第一個yield的值被忽略。(這條來自鳥哥博客的總結,可以看“多任務協作”部分的例子理解)
幾個經典的例子幫助理解!
1.經典的例子熱身
function xrange($start, $end, $step = 1) { for ($i = $start; $i <= $end; $i += $step) { yield $i; } } foreach (xrange(1, 1000000) as$num) { echo$num, "\n"; }
$range = xrange(1, 1000000); var_dump($range); // object(Generator)#1
var_dump($range instanceof Iterator); // bool(true)
2.鳥哥博客中的例子
在PHP中使用協程實現多任務調度,(確定要搞清楚為什按照這種方式輸出. 以便后續繼續閱讀.的例子)在上邊連接文章里有詳細的解釋了。
3.一個讀取文件的例子,同時使用了send函數
/* a.log的內容 */ aaaaa bbbbb ccccc ddddd
function lineGenerator($file) { $fp = fopen($file, 'rb'); while ($line = fgets($fp)) { var_dump(yield $line); } } $lines = lineGenerator("a.log"); foreach ($lines as $line) { $lines->send('test'); echo $line; }
/* 輸出結果 */
string(4) "test" aaaaa NULLstring(4) "test" ccccc NULL
之前對於這個輸出結果一直理解不了,現在總結一下怎么分析:
1.外層循環的每一次都會調用一次內層函數中yield的一行,執行完一次yield便停止執行。
2.對於var_dump(yield $line) 這樣的寫法,在分析時可以拆分為$var = (yield $line);var_dump($var);便於理解。
3.send()函數在調用時如果yield函數一次也沒被執行過,則會先執行一次yield(其實是在創建迭代器時已經隱式的執行了rewind方法),再進行賦值,再執行next(send有一個next的功能)。
根據上邊的方法分析一下上邊的例子:
1.首先,根據總結的方法2先對lineGenerator函數進行修改,因為a.log就4行,索性可以不用循環了。修改后如下:
function lineGenerator($file) { $fp = fopen($file, 'rb'); $var = (yield fgets($fp)); var_dump($var); $var = (yield fgets($fp)); var_dump($var); $var = (yield fgets($fp)); var_dump($var); $var = (yield fgets($fp)); var_dump($var); }
2.foreach開始時,($lines as $line)肯定是調用了current()方法賦值$line,就是內存循環要執行一次yield,此時內層代碼執行到第二行,$line被賦值aaaaa。
3.接着,外層循環調用了$lines->send('test'),這時的test值會賦值給內層函數當前的yield(就是第一個yield),然后執行第一個var_dump(),打印出了test,然后執行第二個yield,並把yield的值賦給send函數的返回值(就是bbbbb),內層代碼停止。
4.接着,外層循環執行了echo $line;所以打印出aaaaa。
5.foreach 進入下次循環,就是需要從函數上次停下的位置執行到下一個yield執行完,函數先執行第二個var_dump(),此時當前的yield是NULL,所 以打印出NULL,接着執行第三個yield,獲取到a.log的第三行賦值給$line,即ccccc,函數執行停止。
6.然后循環執行$lines->send('test'),函數的第三個var_dump()就打印出test,執行第4個yield,把a.log的ddddd賦給send的返回值。函數執行停止。
7.外層循環執行echo $line;打印出ccccc。
8.foreach進入下一次循環,函數又要從上次停止的位置執行到下一個yield結束,就是函數中最后一個var_dump()執行,打印出NULL,因為后邊沒有yield了,代碼執行結束。
4.一個日志寫入的例子,可以說明調用send時沒有調用過yield的情況,用總結的第三個方法解決。
function logger($fileName) { $fileHandle = fopen($fileName, 'a'); while (true) { echo "aaa\n"; fwrite($fileHandle, yield."\n"); echo "bbb\n"; } } $logger = logger('a.log'); var_dump($logger->send('Foo')); var_dump($logger->send('Bar'));
/* 輸出結果*/ aaa bbb aaa NULL bbb aaa NULL
分析:
1.外部第一行$logger = logger('a.log'),此時只是生成了一個Generator對象,yield並沒有執行。
2.外部第二行$logger->send('Foo'),由於函數內部沒有yield,所以會先執行一次yield后再執行賦值,next()操作。執行一次yield,會打印出aaa,注意yield執行完但是fwrite並沒有執行,(這個可以算是send函數的初始化操作,哈哈)
接着才是像正常情況下的send執行一樣,進行next()操作,先把Foo賦值給當前的yield,執行fwrite寫入,然后打印bbb,再打印aaa,執行
fwrite($fileHandle,yield . "\n");注意fwrite不執行,由於yield后邊是空的,所以send的返回值賦值為NULL,內部函數停止,外部的第一個var_dump()會打印一個NULL。
$lineParts = explode(' ', $line, 2); yield $lineParts[0] => $lineParts[1];