生成器提供了一種更容易的方法來實現簡單的對象迭代,相比較定義類實現 Iterator 介面的方式,性能開銷和複雜性大大降低。
生成器總覽
(PHP 5 >= 5.5.0, PHP 7)
生成器提供了一種更容易的方法來實現簡單的對象迭代,相比較定義類實現 Iterator 介面的方式,性能開銷和複雜性大大降低。
生成器允許你在 foreach 代碼塊中寫代碼來迭代一組數據而不需要在記憶體中創建一個數組, 那會使你的記憶體達到上限,或者會占據可觀的處理時間。相反,你可以寫一個生成器函數,就像一個普通的自定義函數一樣, 和普通函數只返回一次不同的是, 生成器可以根據需要 yield 多次,以便生成需要迭代的值。
一個簡單的例子就是使用生成器來重新實現 range() 函數。 標準的 range() 函數需要在記憶體中生成一個數組包含每一個在它範圍內的值,然後返回該數組, 結果就是會產生多個很大的數組。 比如,調用 range(0, 1000000) 將導致記憶體占用超過 100 MB。
做為一種替代方法, 我們可以實現一個 xrange() 生成器, 只需要足夠的記憶體來創建 Iterator 對象併在內部跟蹤生成器的當前狀態,這樣只需要不到1K位元組的記憶體。
Example #1 將 range() 實現為生成器
<?php function xrange($start, $limit, $step = 1) { if ($start < $limit) { if ($step <= 0) { throw new LogicException('Step must be +ve'); } for ($i = $start; $i <= $limit; $i += $step) { yield $i; } } else { if ($step >= 0) { throw new LogicException('Step must be -ve'); } for ($i = $start; $i >= $limit; $i += $step) { yield $i; } } } /* * 註意下麵range()和xrange()輸出的結果是一樣的。 */ echo 'Single digit odd numbers from range(): '; foreach (range(1, 9, 2) as $number) { echo "$number "; } echo "\n"; echo 'Single digit odd numbers from xrange(): '; foreach (xrange(1, 9, 2) as $number) { echo "$number "; } ?>
以上常式會輸出:
Single digit odd numbers from range(): 1 3 5 7 9 Single digit odd numbers from xrange(): 1 3 5 7 9
Generator objects
When a generator function is called for the first time, an object of the internal Generator class is returned. This object implements the Iterator interface in much the same way as a forward-only iterator object would, and provides methods that can be called to manipulate the state of the generator, including sending values to and returning values from it.
生成器語法
一個生成器函數看起來像一個普通的函數,不同的是普通函數返回一個值,而一個生成器可以 yield 生成許多它所需要的值。
當一個生成器被調用的時候,它返回一個可以被遍歷的對象.當你遍歷這個對象的時候(例如通過一個foreach迴圈),PHP 將會在每次需要值的時候調用生成器函數,併在產生一個值之後保存生成器的狀態,這樣它就可以在需要產生下一個值的時候恢復調用狀態。
一旦不再需要產生更多的值,生成器函數可以簡單退出,而調用生成器的代碼還可以繼續執行,就像一個數組已經被遍歷完了。
Note:
一個生成器不可以返回值: 這樣做會產生一個編譯錯誤。然而return空是一個有效的語法並且它將會終止生成器繼續執行。
yield關鍵字
生成器函數的核心是yield關鍵字。它最簡單的調用形式看起來像一個return申明,不同之處在於普通return會返回值並終止函數的執行,而yield會返回一個值給迴圈調用此生成器的代碼並且只是暫停執行生成器函數。
Example #1 一個簡單的生成值的例子
<?php function gen_one_to_three() { for ($i = 1; $i <= 3; $i++) { //註意變數$i的值在不同的yield之間是保持傳遞的。 yield $i; } } $generator = gen_one_to_three(); foreach ($generator as $value) { echo "$value\n"; } ?>
以上常式會輸出:
1 2 3
Note:
在內部會為生成的值配對連續的整型索引,就像一個非關聯的數組。
如果在一個表達式上下文(例如在一個賦值表達式的右側)中使用yield,你必須使用圓括弧把yield申明包圍起來。 例如這樣是有效的:
$data = (yield $value);
而這樣就不合法,並且在PHP5中會產生一個編譯錯誤:
$data = yield $value;
The parenthetical restrictions do not apply in PHP 7.
這個語法可以和生成器對象的Generator::send()方法配合使用。
指定鍵名來生成值
PHP的數組支持關聯鍵值對數組,生成器也一樣支持。所以除了生成簡單的值,你也可以在生成值的時候指定鍵名。
如下所示,生成一個鍵值對與定義一個關聯數組十分相似。
Example #2 生成一個鍵值對
<?php /* * 下麵每一行是用分號分割的欄位組合,第一個欄位將被用作鍵名。 */ $input = <<<'EOF' 1;PHP;Likes dollar signs 2;Python;Likes whitespace 3;Ruby;Likes blocks EOF; function input_parser($input) { foreach (explode("\n", $input) as $line) { $fields = explode(';', $line); $id = array_shift($fields); yield $id => $fields; } } foreach (input_parser($input) as $id => $fields) { echo "$id:\n"; echo " $fields[0]\n"; echo " $fields[1]\n"; } ?>
以上常式會輸出:
1: PHP Likes dollar signs 2: Python Likes whitespace 3: Ruby Likes blocks
和之前生成簡單值類型一樣,在一個表達式上下文中生成鍵值對也需要使用圓括弧進行包圍:
$data = (yield $key => $value);
生成null值
Yield可以在沒有參數傳入的情況下被調用來生成一個 NULL
值並配對一個自動的鍵名。
Example #3 生成NULL
s
<?php function gen_three_nulls() { foreach (range(1, 3) as $i) { yield; } } var_dump(iterator_to_array(gen_three_nulls())); ?>
以上常式會輸出:
array(3) { [0]=> NULL [1]=> NULL [2]=> NULL }
使用引用來生成值
生成函數可以像使用值一樣來使用引用生成。這個和returning references from functions(從函數返回一個引用)一樣:通過在函數名前面加一個引用符號。
Example #4 使用引用來生成值
<?php function &gen_reference() { $value = 3; while ($value > 0) { yield $value; } } /* * 我們可以在迴圈中修改$number的值,而生成器是使用的引用值來生成,所以gen_reference()內部的$value值也會跟著變化。 */ foreach (gen_reference() as &$number) { echo (--$number).'... '; } ?>
以上常式會輸出:
2... 1... 0...
Generator delegation via yield from ¶
In PHP 7, generator delegation allows you to yield values from another generator, Traversable object, or array by using the yield from keyword. The outer generator will then yield all values from the inner generator, object, or array until that is no longer valid, after which execution will continue in the outer generator.
If a generator is used with yield from, the yield from expression will also return any value returned by the inner generator.
Example #5 Basic use of yield from
<?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
Example #6 yield from and return values
<?php function count_to_ten() { yield 1; yield 2; yield from [3, 4]; yield from new ArrayIterator([5, 6]); yield from seven_eight(); return yield from nine_ten(); } function seven_eight() { yield 7; yield from eight(); } function eight() { yield 8; } function nine_ten() { yield 9; return 10; } $gen = count_to_ten(); foreach ($gen as $num) { echo "$num "; } echo $gen->getReturn(); ?>
以上常式會輸出:
1 2 3 4 5 6 7 8 9 10
Comparing generators with Iterator objects
The primary advantage of generators is their simplicity. Much less boilerplate code has to be written compared to implementing an Iterator class, and the code is generally much more readable. For example, the following function and class are equivalent:
<?php function getLinesFromFile($fileName) { if (!$fileHandle = fopen($fileName, 'r')) { return; } while (false !== $line = fgets($fileHandle)) { yield $line; } fclose($fileHandle); } // versus... class LineIterator implements Iterator { protected $fileHandle; protected $line; protected $i; public function __construct($fileName) { if (!$this->fileHandle = fopen($fileName, 'r')) { throw new RuntimeException('Couldn\'t open file "' . $fileName . '"'); } } public function rewind() { fseek($this->fileHandle, 0); $this->line = fgets($this->fileHandle); $this->i = 0; } public function valid() { return false !== $this->line; } public function current() { return $this->line; } public function key() { return $this->i; } public function next() { if (false !== $this->line) { $this->line = fgets($this->fileHandle); $this->i++; } } public function __destruct() { fclose($this->fileHandle); } } ?>
This flexibility does come at a cost, however: generators are forward-only iterators, and cannot be rewound once iteration has started. This also means that the same generator can't be iterated over multiple times: the generator will need to either be rebuilt by calling the generator function again, or cloned via the clone keyword.
摘自:http://php.net/manual/zh/language.generators.php