生成器提供了一種更容易的方法來實現簡單對象的迭代,相比較定義類實現Iterator接口的方式,性能開銷和複雜性大大降低。
生成器語法
一個生成器函數看起來像一個普通函數,不同的是普通函數返回一個值,而生成器可以yield
生成許多它所需要的值。
當一個生成器被調用的時候,它返回一個可以被遍歷的對象。當你遍歷這個對象的時候(如通過foreach循環),PHP將會在每次需要值的時候調用生成器函數,並在產生一個值之後保存生成器的狀態,這樣它就可以在需要產生下一個值的時候恢復調用狀態。當不再需要產生更多的值時,生成器函數可以簡單退出,而調用生成器的代碼還可以繼續執行,就像一個數組已經遍歷完了。
生成器不能通過return返回一個值,這樣做會產生一個編譯錯誤。但是可以通過return空來終止生成器繼續執行。
yield
關鍵字
生成器函數的核心是yield
關鍵字。它最簡單的調用形式看起來像一個return聲明,不同之處在於普通return會返回值並終止函數的執行,而yield
會返回一個值給循環調用此生成器的代碼並且只是暫停執行生成器函數。
一個典型的yield表達式:$data = yield $key => $value;
以上yield表達式在PHP5需要加上括號
$data = (yield $key=>$value);
,否則會產生一個編譯錯誤,PHP7則不需要關心括號問題。
這個表達式包含了兩個部分:
- yield後面的
$key =>$valeue
,這裏是鍵值對,它也可以是單個值,如下例中的$i
。這部分表達式是返回給上層調用的,也就是上層可以通過 current 方法接收到值或者在執行 send 方法的返回值; - yield本身,會收到 send 方法傳入的值,這個值就是整個 yield 表達式當前的值,可以被左邊的變量接收。
【示例】
<?php
// 創建生成器
function gen_one_to_three() {
for ($i = 1; $i <= 3; $i++) {
// 注意變量$i的值在不同的yield之間是保持傳遞的
yield $i;
}
}
// 使用生成器
$generator = gen_one_to_three();
var_dump($generator);
echo "<br />";
foreach($generator as $value) {
echo "$value <br />";
}
?>
程序輸出結果:
object(Generator)#1 (0) { }
1
2
3
通過以上代碼可以看出,生成器函數返回的是一個Generator對象,Generator類摘要如下:
Generator implements Iterator
{
/**
* 返回當前產生的值(yield後面表達式的值)
*/
public mixed current ( void )
/**
* 返回當前產生的鍵(yield 'key'=>'val';)
*/
public mixed key ( void )
/**
* 從上一個yield之後繼續執行,直到下一個yield
*/
public void next ( void )
/**
* 重置迭代器(在整個循環開始前被調用(也就是生成
* Generator對象時),這樣保證了我們多次遍歷得到
* 的結果都是一致的)
*/
public void rewind ( void )
/**
* 向生成器中傳入一個值,並從上一個yield之後繼續執行
*/
public mixed send ( mixed $value )
/**
* 向生成器中拋出一個異常,並從上一個yield之後繼續執行
*/
public void throw ( Exception $exception )
/**
* 檢查迭代器是否被關閉(false表示已關閉)
*/
public bool valid ( void )
/**
* 序列化回調,該函數會拋出一個異常以表示生成器不能
* 被序列化。
*/
public void __wakeup ( void )
}
從以上代碼中可以看出,生成器實現了Iterator
接口,因此生成器對象是可迭代的,在實現Iterator
接口的基礎上還新增了throw
、send
、__wakeup
方法。
我們畫個圖來解釋一下最開始的示例代碼:
當Generator對象對foreach的時候,內部的valid,current,key,send,next方法會被依次調用。
yield的特點:
- yield只能用於函數內部,在非函數內部運用會拋出錯誤。
- 如果函數包含了yield關鍵字,那麼函數執行後的返回值永遠是一個Generator對象。
- 如果函數內部同時包含yield和return,該函數依然返回一個Generator對象,但在生成Generator對象時,return語句後面的代碼會被忽略。
- 一旦返回的Generator對象被遍歷完成,便不能調用他的rewind方法來重置。
- Generator對象不能被clone關鍵字克隆。
- Generator對象不能被序列化。
- Generator類實現了Iterator接口。
- 可以通過send方法給yield關鍵字賦一個值。
- 可以通過Generator對象的內部方法,獲取到函數內部yield後面表達式的值。
多個yield語句
通常函數只能執行一次return語句,而yield卻可以執行多個。
【示例】
<?php
function mygen(){
echo "開始執行yield<br/>";
yield 1;
echo "i <br/>";
yield 2;
yield 3;
yield 4;
}
$g = mygen();
foreach ($g as $k => $v){
echo "{$k} - {$v} <br/>";
}
?>
程序輸出結果:
開始執行yield
0 - 1
i
1 - 2
2 - 3
3 - 4
通過以上結果可以看出,首先當遍歷開始時rewind被執行,代碼進入mygen()
函數開始執行,首先輸出了開始執行yield
,然後遇到了第一個yield語句,此時調用Generator對象的key()
方法,獲取到當前key
的值爲yield出現的次序爲0,然後調用current()
方法獲得yield表達式後的值也就是1。然後執行valid()
因爲當前爲第一個yield,所以返回true,接下來執行foreach中的語句正常輸出0 - 1,進入下一次迭代,此時next()
方法被執行,跳轉到了第二個yield,第一個到第二個之間的代碼被執行輸出了i。再次進入 執行vaild()
,由於當前在第二個yield上面,所以依然是true,由於next()
執行了,所以key的值也有剛剛的0變爲了1,current的值爲2,正常輸出 1 - 2。再次執行foreach中的語句,進入下一次迭代。繼續執行next()
和vaild()
,由於此時到了第三個yield返回依然是true。key的值爲2, yield爲3。正常輸出 2 -3,再次進入迭代,再次執行next()
,知道沒有後續yield了vaild()
返回爲false, 所以循環到此便終止了。
遍歷Generator對象的每次迭代都只會執行前一次yield語句之後的代碼,而且碰到yield語句就會返回一個值,相當於從generator函數中返回,這有點像掛起一個進程(線程)的執行(yield在很多語言中就是用於掛起進程(線程)),然後又啓動它繼續執行,周而復始直到進程(線程)執行中止。
send()
方法
public mixed Generator::send(mixed $value)
該函數向生成器中傳入一個值,並且當做 yield 表達式的結果,然後繼續執行生成器。如果當這個方法被調用時,生成器不在 yield 表達式,那麼在傳入值之前,它會先運行到第一個 yield 表達式。
send()方法主要用於發送數據給當前yield,即yield表達式被當作一個值被替換,且繼續執行下一個yield,即next()
【示例】
<?php
function gen(){
for($i = 0; $i < 5; $i++){
echo (yield $i).$i.'<br />';
}
}
$gen = gen();
$gen->send(666);
?>
程序輸出結果:
6660
首先把666代替當前yield表達式的值,然後執行next()
,即運行echo (yield i.'<br/>'
,當前yield是666,所以最終結果是:6660。
生成器委託
PHP7新增了yield from關鍵詞,該語法開始允許從其他的 generator,Traversable 對象,或者數組通過 yield from 生成數函數 來 yield 值。yield from 的各種特性與 yield 一樣都是生成數據,只是後面跟隨的表達式不同。
PHP7中,通過生成器委託(yield from),可以將其他生成器、可迭代的對象、數組委託給外層生成器。外層的生成器會先順序 yield 委託出來的值,然後繼續 yield 本身中定義的值。同時yield from也能獲取到生成器的返回值,和生成器的getReturn方法作用同等,需要注意這裏僅僅指的是獲取返回值是同等的。
【示例】
<?php
function count_to_ten() {
yield 1;
yield 2;
yield from [3, 4]; // 數組
yield from new ArrayIterator([5, 6]); // 可遍歷對象
yield from seven_eight(); // 生成器
yield 9;
yield 10;
}
function seven_eight() {
yield 7;
yield from eight();
}
function eight() {
yield 8;
}
foreach(count_to_ten() as $num) {
echo "$num <br />";
}
?>
程序輸出結果:
1
2
3
4
5
6
7
8
9
10
以上代碼中生成器count_to_ten()
分別委託了數組,可迭代對象,以及生成器。count_to_ten()
在遇到委託的時候會順序迭代委託,然後再繼續本身的yield。yield from 以方便我們編寫比較清晰生成器嵌套,這點可以類比於函數中的嵌套調用,當函數 A 中調用另一個函數 B,此時會等 B 執行完成並返回,方纔繼續執行。