PHP yield 分析,以及協程的實現,超詳細版(上)
參考資料
- http://www.laruence.com/2015/05/28/3038.html
- http://php.net/manual/zh/class.generator.php
- http://www.cnblogs.com/whoamme/p/5039533.html
- http://php.net/manual/zh/class.iterator.php
PHP的 yield 關鍵字是php5.5版本推出的一個特性,算是比較古老的了,其他很多語言中也有類似的特性存在。但是在實際的項目中,目前用到還比較少。網上相關的文章最出名的就是鳥哥的那篇了,但是都不夠細致理解起來較為困難,今天我來給大家超詳細的介紹一下這個特性。
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對象被遍歷的時候內部是怎麽工作的,我將代碼改了一下,將接口內的每個方法都盡心輸出,借此來窺探一下遍歷時對象內部方法的的執行情況。
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...View Code
看到這裏,我相信大家對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, 所以循環到此便終止了。
下面我們用代碼來驗證一下
$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目前對我們來說,似乎無太大的用處。當然我們可以利用他來生成一些集合對象,節約一些內存知道數據真正被用到的時候在生成。例如:
我們可以寫一個方法
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語句的值。
首先大家考慮一下下面的代碼
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特性較多也較為繁瑣。文章內容難免有出錯或者不周全的地方,如果大家發現有錯誤的地方,也希望大家留言告知, 祝大家周末愉快~
PHP yield 分析,以及協程的實現,超詳細版(上)