淺談 PHP 生成器

zhengzean1發表於2018-01-24

前言

本來是打算寫一篇關於PHP協程排程的文章的,結果寫的時候發現裡面太多點都是需要介紹生成器,所以還是先單獨寫一篇介紹一下PHP生成器的相關知識作為前作吧。

生成器

首先讓我們看下PHP官方文件中是怎麼介紹的吧:

生成器提供了一種更容易的方法來實現簡單的物件迭代,相比較定義類實現 Iterator 介面的方式,效能開銷和複雜性大大降低。

看了這句話,我們可以獲得幾個關鍵詞: 物件迭代、 Iterator 介面、效能開銷 ,比較抽象,talk is cheap show me the code,下面我們從一個生成器最經典的例子開始吧。

PHP中的range() 函式在使用的時候會在記憶體中建立一個包含指定範圍單元的陣列並返回,一般來說,這個並沒什麼不妥,但是當所傳的limit入參值很大的時候,那麼也就意味著將會在記憶體中建立的陣列也會很大,這個就太恐怖了,這是要幹爆記憶體的節奏啊。此時我們可以通過生成器來實現一個更高效的range函式,(下面的程式碼是把PHP官方文件中精簡處理了一下):

function xrange($start, $limit, $step = 1)
{
    //校驗引數,此處省略

    for ($i = $start; $i <= $limit; $i += $step) {
        //向外產出值
        yield $i;
    }
}

//xrange此時返回的是一個生成器物件
$gen = xrange(1, 9);

//對生成器進行迭代
foreach ($gen as $number) {
    echo "$number ";
}

這裡在xrange和range函式的效果相同,均是產生了一個可迭代的變數,但是不同的是,range函式有點像ORM裡面常說的 預載入 ,而xrange則是 懶載入 只是等到迭代到那個點才會產生對應的值,因此xrange並不需要分配大塊記憶體來存放變數,大大節約了記憶體,提升效率。

現在我們來總結下生成器和普通函式有哪些異同:

  1. 生成器中必須包含yield關鍵字(用來生成結果),而且可以是多次出現,普通函式中向外部返回結果只能使用return,且函式執行完畢;
  2. 一個生成器不可以通過return返回值,這樣做會產生一個編譯錯誤PHP Fatal error: Generators cannot return values using "return"注意:這個在PHP7下面不會出錯,但是會終止生成器繼續執行,即調valid()方法會返回false,然而在PHP5中return空是一個有效的語法並且它將會終止生成器繼續執行)

生成器類(Generator)

Generator 物件是從生成器返回的,上面程式碼中$gen就是一個生成器物件。注意,生成器物件和其他類的物件不同,它並不能通過new關鍵字建立,只能從生成器函式獲取。首先我們看下Generator類摘要來看看其組成:

Generator implements Iterator
{
    /**
     * 返回當前產生的值(yield後面表示式的值)
     */
    public mixed current ( void )

    /**
     * yield的鍵(yield 'key'=>'val';)
     */
    public mixed key ( void )

    /**
     * 從上一個yield之後繼續執行,直到下一個yield
     */
    public void next ( void )

    /**
     * 重置迭代器(對於生成器並沒什麼卵用)
     */
    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 )
}

從上面的類摘要可以看出,Generator類是實現Iterator介面的,因此它具有迭代器的的特性。另外它加入了send()throw()__wekeup()方法,相關方法說明已經寫了註釋,在此不再贅述。

寫了一堆,發現我的文筆不好,還是畫個圖感受一下吧(圖畫的也不美觀,大家湊合著看吧,2333)
file

yield關鍵詞

接下來讓我們看下yield關鍵詞,它最簡單的呼叫形式看起來像普通函式的return,不同之處在於普通return會返回值並終止函式的執行,而yield會返回一個值給迴圈呼叫此生成器的程式碼,並且只是暫停執行生成器函式。

這是一個典型的yield表示式:$data = yield $key => $value;,該表示式包括兩部分:

注意:PHP5需要加上括號$data = (yield $key => $value);,否則會產生一個編譯錯誤,PHP7就不用關心這個了

  • 其一,是yield後面的表示式,這個可以是單個值也可以是鍵值對,與array中的一個元素對應,這部分表示式是返回給上層呼叫的,也就是上層可以通過current方法接收到值或者在執行send方法的返回值;
  • 另外一塊就是yield關鍵詞本身,個人把他理解成一個接收器,會收到send方法傳入的值,這個值就是整個yield表示式當前的值,可以被左邊的變數接收。

這麼說可能有點抽象,還是上圖吧:
file

生成器委託(yield from)

PHP7新增了yield from關鍵詞,該語法開始允許從其他的generator,Traversable物件,或者陣列通過yield from 生成數函式 來yield值。yield from的各種特性與yield一樣都是生成資料,只是後面跟隨的表示式不同。下面看個例子(摘自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 ";
}

//輸出:1 2 3 4 5 6 7 8 9 10 

yield from以方便我們編寫比較清晰生成器巢狀,這點可以類比於函式中的巢狀呼叫,當函式A中呼叫另一個函式B,此時會等B執行完成並返回,方才繼續執行。在沒有yield from的時候,實現生成器巢狀需要自己實現棧並進行壓棧和彈出操作以達到相同效果,那是多麼痛苦的操作。

結束

好了,這就是我自己看生成器官方文件的一些收穫總結,裡面程式碼均是改編自文件,大家也可以自己直接研究文件。同時大家如果發現有什麼錯誤,也請批評指出。

文中參考文件:

生成器
生成器類

只要全力以赴就無所謂失敗