如何使用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的所有功能都可以使用傳統函式搭配
Iterator
interfae達成,但generator更簡單、更快速、且更節省記憶體,但也只提供forward-only功能。