參考資料php
PHP的 yield 關鍵字是php5.5版本推出的一個特性,算是比較古老的了,其餘不少語言中也有相似的特性存在。可是在實際的項目中,目前用到還比較少。網上相關的文章最出名的就是鳥哥的那篇了,可是都不夠細緻理解起來較爲困難,今天我來給你們超詳細的介紹一下這個特性。
html
function gen(){ while(true){ yield "gen\n"; } } $gen = gen(); var_dump($gen instanceof Iterator); echo "hello, world!";
若是事先沒了解過yield,可能會以爲這段代碼必定會進入死循環。可是咱們將這段代碼直接運行會發現,輸出hello, world!,預想的死循環沒出現。
到底是什麼樣的力量,征服了while(true)呢,接下來就帶你們一塊兒來領略一下yield關鍵字的魅力。
首先要從foreach提及,咱們都知道對象,數組和對象能夠被foreach語法遍歷,數字和字符串缺不行。其實除了數組和對象以外PHP內部還提供了一個 Iterator 接口,實現了Iterator接口的對象,也是能夠被foreach語句遍歷,固然跟普通對象的遍歷就很不同了。
如下面的代碼爲例:數組
class Number implements Iterator{ protected $key; protected $val; protected $count; public function __construct(int $count){ $this->count = $count; } public function rewind(){ $this->key = 0; $this->val = 0; } public function next(){ $this->key += 1; $this->val += 2; } public function current(){ return $this->val; } public function key(){ return $this->key + 1; } public function valid(){ return $this->key < $this->count; } } foreach (new Number(5) as $key => $value){ echo "{$key} - {$value}\n"; }
這個例子將輸出
1 - 0
2 - 2
3 - 4
4 - 6
5 - 8
關於上面的number對象,被遍歷的過程。若是是初學者,可能會出現有點懵的狀況。爲了深刻的瞭解Number對象被遍歷的時候內部是怎麼工做的,我將代碼改了一下,將接口內的每一個方法都盡心輸出,藉此來窺探一下遍歷時對象內部方法的的執行狀況。ide
class Number implements Iterator{ protected $i = 1; protected $key; protected $val; protected $count; public function __construct(int $count){ $this->count = $count; echo "第{$this->i}步:對象初始化.\n"; $this->i++; } public function rewind(){ $this->key = 0; $this->val = 0; echo "第{$this->i}步:rewind()被調用.\n"; $this->i++; } public function next(){ $this->key += 1; $this->val += 2; echo "第{$this->i}步:next()被調用.\n"; $this->i++; } public function current(){ echo "第{$this->i}步:current()被調用.\n"; $this->i++; return $this->val; } public function key(){ echo "第{$this->i}步:key()被調用.\n"; $this->i++; return $this->key; } public function valid(){ echo "第{$this->i}步:valid()被調用.\n"; $this->i++; return $this->key < $this->count; } } $number = new Number(5); echo "start...\n"; foreach ($number as $key => $value){ echo "{$key} - {$value}\n"; } echo "...end...\n";
以上代碼輸出以下函數
第1步:對象初始化. start... 第2步:rewind()被調用. 第3步:valid()被調用. 第4步:current()被調用. 第5步:key()被調用. 0 - 0 第6步:next()被調用. 第7步:valid()被調用. 第8步:current()被調用. 第9步:key()被調用. 1 - 2 第10步:next()被調用. 第11步:valid()被調用. 第12步:current()被調用. 第13步:key()被調用. 2 - 4 第14步:next()被調用. 第15步:valid()被調用. 第16步:current()被調用. 第17步:key()被調用. 3 - 6 第18步:next()被調用. 第19步:valid()被調用. 第20步:current()被調用. 第21步:key()被調用. 4 - 8 第22步:next()被調用. 第23步:valid()被調用. ...end...
看到這裏,我相信你們對Iterator接口已經有必定認識了。會發現當對象被foreach的時候,內部的valid,current,key方法會依次被調用,其返回值即是foreach語句的key和value。循環的終止條件則根據valid方法的返回而定。若是返回的是true則繼續循環,若是是false則終止整個循環,結束遍歷。當一次循環體結束以後,將調用next進行下一次的循環直到valid返回false。而rewind方法則是在整個循環開始前被調用,這樣保證了咱們屢次遍歷獲得的結果都是一致的。
那麼這個跟yield有什麼關係呢,這即是咱們接下來要說的重點了。首先給你們介紹一下我總結出來的 yield 的特性,包含如下幾點。
1.yield只能用於函數內部,在非函數內部運用會拋出錯誤。
2.若是函數包含了yield關鍵字的,那麼函數執行後的返回值永遠都是一個Generator對象。
3.若是函數內部同事包含yield和return 該函數的返回值依然是Generator對象,可是在生成Generator對象時,return語句後的代碼被忽略。
4.Generator類實現了Iterator接口。
5.能夠經過返回的Generator對象內部的方法,獲取到函數內部yield後面表達式的值。
6.能夠經過Generator的send方法給yield 關鍵字賦一個值。
7.一旦返回的Generator對象被遍歷完成,便不能調用他的rewind方法來重置
8.Generator對象不能被clone關鍵字克隆
首先看第1點,能夠明白咱們文章開頭的gen函數執行後返回的是一個Generatory對象,因此代碼能夠繼續執行下去輸出hello, world!,所以$gen是一個Generator對象,因爲其實現了Iterator,因此這個對象能夠被foreach語句遍歷。下面咱們來看看對其進行遍歷,會是什麼樣的效果。爲了防止被死循環,我加多了一個break語句只進行十次循環,方便咱們瞭解yield的一些特性。
代碼以下:性能
$i = 0; foreach ($gen as $key => $value) { echo "{$key} - {$value}"; if(++$i >= 10){ break; } }
以上代碼輸出爲
0 - gen
1 - gen
2 - gen
3 - gen
4 - gen
5 - gen
6 - gen
7 - gen
8 - gen
9 - gen
經過觀察不難發現其中的規律。在包含yield的函數返回的對象被foreach遍歷時, 函數體內部的代碼會被對應的執行。PHP 會分析其內部的代碼從而生成對應的Iterator接口的方法。
其中key方法實現是返回的是yield出現的次序,從0開始遞增。
current方法則是yield後面表達式的值。
而valid方法則在當前yield語句存在的時候返回true, 若是當前不在yield語句的時候返回false。
next方法則執行從當前到下一個yield、或者return、或者函數結束之間的代碼。
網上也有文章讓你們把yield理解爲暫時中止函數的執行,等待外部的激活從而再次執行。雖然看起來確實像那麼回事,但我不建議你們這麼理解,由於他自己是返回一個迭代器對象,其返回值是能夠被用於迭代的。咱們理解了他被foreach迭代時,其內部是如運做的以後更易於理解yield關鍵字的本質。
下面咱們再作一個簡單的測試,以便更直觀的展現他的特性。測試
function gen1(){ yield 1; echo "i\n"; yield 2; yield 3+1; } $gen = gen1(); foreach ($gen as $key => $value) { echo "{$key} - {$value}\n"; }
以上的代碼輸出
0 - 1
i
1 - 2
2 - 4
咱們來分析一下輸出的結果,首先當遍歷開始時rewind被執行因爲第一個yield以前無任何語句,無任何輸出。
key的值爲yield出現的次序爲0,current爲yield表達式後的值也就是1。
foreach開始,valid由於當前爲第一個yield,因此返回true。正常輸出0 - 1
此時next方法被執行,跳轉到了第二個yield,第一個到第二個之間的代碼被執行輸出了i。
再次進入循環 執行vaild,因爲當前在第二個yield上面,因此依然是true
因爲next執行了,因此key的值也有剛剛的0變爲了1,current的值爲2,正常輸出 1 - 2。
這時候繼續執行next(),進入循環vaild()執行,因爲此時到了第三個yield返回依然是true。key的值爲2, yield爲4。正常輸出 2 - 4
再次執行next(),因爲後續沒有yield了vaild()返回爲false, 因此循環到此便終止了。
下面咱們用代碼來驗證一下this
$gen = gen1(); var_dump($gen->valid()); echo $gen->key().' - '.$gen->current()."\n"; $gen->next(); var_dump($gen->valid()); echo $gen->key().' - '.$gen->current()."\n"; $gen->next(); var_dump($gen->valid()); echo $gen->key().' - '.$gen->current()."\n"; $gen->next(); var_dump($gen->valid());
輸出值以下
bool(true)
0 - 1
i
bool(true)
1 - 2
bool(true)
2 - 4
bool(false)
跟咱們的分析徹底一致,至此咱們瞭解了Iterator接口在遍歷時內部的運做方式,也瞭解了包含yield關鍵字的函數所生成的對象內部是如何實現Iterator接口的方法的。對於yild的特性瞭解一半了,可是若是咱們僅僅將其用於生成能夠被遍歷的對象的話,yield目前對咱們來講,彷佛無太大的用處。固然咱們能夠利用他來生成一些集合對象,節約一些內存知道數據真正被用到的時候在生成。例如:
咱們能夠寫一個方法spa
function gen2(){ yield getUserData(); yield getBannerList(); yield getContext(); } #中間其餘操做 #而後在view中得到數據 $data = gen2(); foreach ($data as $key => $value) { handleView($key, $value); }
經過以上的代碼,咱們將幾個獲取數據的操做都延遲到了數據被渲染的時候執行。節省了中間進行其餘操做時獲取回來的數據佔用的內存空間。然而實際開放項目的過程當中,這些數據每每被多處使用。並且這樣的結構讓咱們單獨控制數據變得艱難,以此帶來的性能提高相對於便利性來講,好處微乎其微。不過還好的是,咱們對yield的瞭解纔剛剛到一半,已經有這樣的功效了。相信咱們在瞭解完另一半以後,它的功效將大大提高。
接下來咱們來繼續瞭解yield, 因爲yield返回的是一個Generator類的對象,這個對象除了實現了Iterator接口以外,內部還有一個至關重要的方法就是send方法,即咱們提到的第6點特性,經過send方法咱們能夠給yield發送一個值做爲yield語句的值。
首先你們考慮一下下面的代碼.net
function gen3(){ echo "test\n"; echo (yield 1)."I\n"; echo (yield 2)."II\n"; echo (yield 3 + 1)."III\n"; } $gen = gen3(); foreach ($gen as $key => $value) { echo "{$key} - {$value}\n"; }
執行之後輸出
0 - 1
I
1 - 2
II
2 - 4
III
可能這段輸出比較難理解,咱們接下來,一步一步分析一下爲何得出這樣的輸入。因爲咱們知道了foreach的時候gen內部是如何操做的,那麼咱們便用代碼來實現一次。
$gen = gen3(); $gen->rewind(); echo $gen->key().' - '.$gen->current()."\n"; $gen->next();
執行後輸出
0 - 1
I
經過這兩句咱們發現,當前的key爲0,current則爲1也就是yield後面表達式的值。由於yield 1被括號括起來了,因此yield後面表達式的值是1,若是沒有括號則爲1."I\n".固然由於1."I\n"是一個錯誤語法。若是想要測試的朋友須要給1加上雙引號。
當執行next時,第1個yield到第二個yieldz之間的的語法被執行。也就是echo (yield 1)."I\n"被執行了,因爲咱們使用的是next(),因此yield當前是無值的。因此輸出了I。須要注意的是在第一個yield以後的語法將不會被執行,而 echo (yield 2). "II\n";屬於下一個yield塊的語句,因此不會被執行。
到這裏,是時候讓咱們今天最後的主角send方法來表現一下了。
public mixed Generator::send ( mixed $value )
這個是手冊裏send方法的描述,能夠看出來他能夠接受一個mixed類型的參數,也會返回一個mixed類型的值。
傳入的參數會被作 yield 關鍵字在語句中的值,而他的返回值則是next以後,$gen->current()的值。
下面咱們來嘗試一下
$gen = gen3(); $gen->rewind(); echo $gen->key().' - '.$gen->current()."\n"; echo $gen->send("send value - ");
執行後輸出
0 - 1
send value - I
2
這時候咱們發現,咱們經過send方法成功的將一個值傳遞給了一個函數的內部,而且當作yield關鍵字的值給輸出了,因爲下一個yield的值爲2,因此咱們調用send返回的值爲2,一樣被輸出。
雖然咱們知道了send能夠完成內部對函數內部的yield表達式傳值,也知道了能夠經過$gen->current()得到當前yield表達式以後的值,可是這個有什麼用呢。能夠看一下這個函數
function gen4(){ $id = 2; $id = yield $id; echo $id; } $gen = gen4(); $gen->send($gen->current() + 3);
根據上面對yield代碼的理解,咱們不難發現這個函數會輸出5,由於current()爲2,而當咱們send以後 yield的值爲 2 + 3,也就是5.同時yield到函數結束之間的代碼被執行。也就是$id = 5; echo $id; 經過這樣一個簡單的例子,咱們發現。咱們不但從函數內部得到了返回值,而且將他的返回值再次發送給了函數內部參與後續的計算。 關於yield的介紹就到此爲止了,本文至此也告一段落。後續將會給你們帶來,關於yield的下篇,實現一個調度器使得咱們只須要將gen()函數返回的gen對象傳遞給調度器,其內部的代碼就能自動的執行。而且讓利用yield來實現並行(僞),以及在多個$gen對象執行之間創建聯繫和控制其執行順序,請你們多多關注。另外因爲本人才疏學淺,yield特性較多也較爲繁瑣。文章內容不免有出錯或者不周全的地方,若是你們發現有錯誤的地方,也但願你們留言告知, 祝你們週末愉快~