如何使用Generator?
在物件導向時代,我們會使用foreach的方式遍訪每一個物件,傳統上我們會採用Iterator Pattern,去實作Iterator interface,簡單地說,generator就是更簡單的iterator。
Generator讓你不須實踐Iterator interface,也不須將資料放在記憶體中,直到foreach要取值時,才及時產生資料使用yield傳回。
Version
PHP 5.5
為什麼要使用Generator?
傳統上我們會將資料準備在記憶體當中,為了讓foreach能遍訪每一筆資料,我們必須實踐Iterator interface。假如資料筆數很龐大,會遇到以下問題 :
- 需要浪費極大的記憶體空間儲存資料。
- 需要浪費寫入記憶體與讀出記憶體的時間。
假如我們能在foreach遍訪每一筆資料時,才動態地產生資料傳回,這樣就不會浪費記憶體,也不用浪費時間讀寫記憶體,這就是generator要做的。
最簡單的Generator
1 | function myGenerator() |
執行結果1
2
3value1
value2
value3
第1行1
2
3
4
5
6function myGenerator()
{
yield 'value1';
yield 'value2';
yield 'value3';
}
因為使用了yield關鍵字,所以搖身一變成了generator函式。
傳統函式會使用return關鍵字,而且一個函式最多只能有一個return。
而generator會使用yield取代return,而且可以有多個yield,回傳的是Generator物件,這是一個已經實踐Iterator interface的物件,可以給foreach使用。
與傳統函式最大的差別在於return回傳值後,函式所在的記憶體在stack會釋放,導致函式內的變數都會不見,但generator函式yield回傳generator物件後,generator函式所在的記憶體stack並沒有釋放,所有函式內的變數都被保留。除此之外,當generator函式再次被呼叫時,會從上一次yield的下一行開始執行,而不是從函式的第一行開始執行。
第8行1
2
3foreach (myGenerator() as $yieldedValue) {
echo $yieldedValue, PHP_EOL;
}
其流程圖如下所示 :
foreach雖然執行了3次myGenerator(),但事實上每次執行的內容都不一樣,第一次只執行到yield 'value1'就結束,而第二次會執行下一行的yield 'value2',最後才執行yield 'value3'。
實作range()
以上簡單的範例只是讓我們了解generator函式是如何運行,但還沒展現其威力,接著我們來實作PHP的range()函式。
若使用傳統寫法,我們可能會這樣實作range()。
1 | function makeRange($length) |
這種寫法雖然可行,但有2個問題 :
- 若傳進
makeRange()的值很大,$dataset會非常佔記憶體。 makeRange()的return $dataset,因為makeRange()執行完stack會釋放,必須將$dataset複製到外面stack記憶體,若傳進makeRange()的值很大,記憶體複製會非常耗時。
若使用generator,就會有新的做法 :1
2
3
4
5
6
7
8
9
10function makeRange($length)
{
for ($i = 0; $i < $length; $i++)
yield $i;
}
foreach (makeRange(1000000) as $i)
{
echo $i, PHP_EOL;
}
我們不必再準備一塊記憶體放$dataset,也不用擔心return $dataset造成記憶體複製的負擔。
取而代之的是每次foreach才去yield值,而每次yield之後,記憶體不會釋放,變數i值會保留,下一次yield就是$i++的值。
讀取4GB的超大檔案
Modern PHP書中還提到另外一個在實務上非常實用的範例,若我們需要讀取一個超大檔案(4GB),但server只分配1GB的記憶給PHP使用,顯然我們不可能再將4GB的檔案放在記憶體來foreach,若使用generator將有很棒的解法。
1 | function getRows($file) |
getRows() generator函式一次只靠yield回傳一行,也就是說,記憶體的需求從原本的4GB降低到只需一行的記憶體。
因為它沒有在記憶體保存所有資料,所以只能算是forward-only iterator,它只知道目前的資料為何,然後靠yield傳回,它無法後退,也無法快轉,若要求更進階的功能,就必須使用Standard PHP Library (SPL) iterator,並且實踐Iterator interface。
Conclusion
- Generator與
yield是PHP很大的syntax sugar,程式執行的方式與傳統函式不太一樣。 - Generator並沒有替PHP增加新的功能,generator的所有功能都可以使用傳統函式搭配
Iteratorinterfae達成,但generator更簡單、更快速、且更節省記憶體,但也只提供forward-only功能。