2024 日本 PHP 大會

產生器語法

產生器函式看起來就像一般的函式,只是它不是回傳值,而是視需要 yield 產出許多值。任何包含 yield 的函式都是產生器函式。

當呼叫產生器函式時,它會回傳一個可迭代的物件。當您迭代該物件時(例如,透過 foreach 迴圈),PHP 會在每次需要值時呼叫物件的迭代方法,然後在產生器產出值時儲存產生器的狀態,以便在需要下一個值時可以恢復。

一旦沒有更多值要產出,產生器就可以簡單地回傳,並且呼叫程式碼會繼續執行,就像陣列的值已經用完一樣。

注意:

產生器可以回傳值,這些值可以使用 Generator::getReturn() 擷取。

yield 關鍵字

產生器函式的核心是 yield 關鍵字。在其最簡單的形式中,yield 敘述看起來很像 return 敘述,只是 yield 不是停止函式的執行並回傳,而是向迴圈處理產生器的程式碼提供值,並暫停產生器函式的執行。

範例 #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

注意:

在內部,循序整數鍵將與產出的值配對,就像非關聯陣列一樣。

使用鍵產出值

PHP 也支援關聯陣列,而產生器也沒什麼不同。除了如上所示產出簡單的值之外,您還可以同時產出鍵。

產出鍵/值對的語法與定義關聯陣列所使用的語法非常相似,如下所示。

範例 #2 產出鍵/值對

<?php
/*
* 輸入是以分號分隔的欄位,第一個欄位是
* 要用作鍵的 ID。
*/

$input = <<<'EOF'
1;PHP;喜歡錢符號
2;Python;喜歡空白
3;Ruby;喜歡區塊
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

產出空值

可以在不使用引數的情況下呼叫 yield,以使用自動鍵產出 null 值。

範例 #3 產出 null

<?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
}

以參考方式產出

產生器函式能夠以參考和值的方式產生值。這與從函式回傳參考的方式相同:在函式名稱前面加上 & 符號。

範例 #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...

透過 yield from 進行產生器委派

產生器委派允許您透過使用 yield from 關鍵字,從另一個產生器、Traversable 物件或 array 產出值。外部產生器接著會從內部產生器、物件或陣列產出所有值,直到該者不再有效為止,之後執行將繼續在外部產生器中。

如果產生器與 yield from 一起使用,yield from 表達式也會回傳內部產生器回傳的任何值。

注意

儲存到陣列中(例如使用 iterator_to_array()

yield from 不會重設鍵。它會保留 Traversable 物件或 array 回傳的鍵。因此,某些值可能會與另一個 yieldyield from 共用一個通用鍵,在插入陣列時,會以該鍵覆寫先前的值。

其中一個重要例子是 iterator_to_array() 預設回傳一個鍵控陣列,導致可能產生非預期的結果。iterator_to_array() 有第二個參數 preserve_keys,可以將其設定為 false 以收集所有值,同時忽略 Generator 回傳的鍵。

範例 #5 與 iterator_to_array() 一起使用的 yield from

<?php
function inner() {
yield
1; // key 0
yield 2; // key 1
yield 3; // key 2
}
function
gen() {
yield
0; // key 0
yield from inner(); // keys 0-2
yield 4; // key 1
}
// 傳遞 false 作為第二個參數以取得陣列 [0, 1, 2, 3, 4]
var_dump(iterator_to_array(gen()));
?>

上面的範例會輸出

array(3) {
  [0]=>
  int(1)
  [1]=>
  int(4)
  [2]=>
  int(3)
}

範例 #6 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

範例 #7 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();
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
新增註解

使用者貢獻的註解 9 個註解

Adil lhan (adilmedya at gmail dot com)
11 年前
例如,使用費波納契數列的 yield 關鍵字

function getFibonacci()
{
$i = 0;
$k = 1; //第一個費波納契值
yield $k;
while(true)
{
$k = $i + $k;
$i = $k - $i;
yield $k;
}
}

$y = 0;

foreach(getFibonacci() as $fibonacci)
{
echo $fibonacci . "\n";
$y++;
if($y > 30)
{
break; // 防止無限迴圈
}
}
info at boukeversteegh dot nl
9 年前
[此評論取代我之前的評論]

您可以使用生成器來執行列表的延遲載入。您只會計算實際使用的項目。但是,當您想要載入更多項目時,要如何快取已載入的項目?

以下是如何使用生成器執行快取延遲載入

<?php
class CachedGenerator {
protected
$cache = [];
protected
$generator = null;

public function
__construct($generator) {
$this->generator = $generator;
}

public function
generator() {
foreach(
$this->cache as $item) yield $item;

while(
$this->generator->valid() ) {
$this->cache[] = $current = $this->generator->current();
$this->generator->next();
yield
$current;
}
}
}
class
Foobar {
protected
$loader = null;

protected function
loadItems() {
foreach(
range(0,10) as $i) {
usleep(200000);
yield
$i;
}
}

public function
getItems() {
$this->loader = $this->loader ?: new CachedGenerator($this->loadItems());
return
$this->loader->generator();
}
}

$f = new Foobar;

# 第一次
print "First\n";
foreach(
$f->getItems() as $i) {
print
$i . "\n";
if(
$i == 5 ) {
break;
}
}

# 第二次 (項目 1-5 已快取,6-10 已載入)
print "Second\n";
foreach(
$f->getItems() as $i) {
print
$i . "\n";
}

# 第三次 (所有項目都已快取並立即返回)
print "Third\n";
foreach(
$f->getItems() as $i) {
print
$i . "\n";
}
?>
Hayley Watson
8 年前
如果因為某些奇怪的原因,您需要一個不產生任何東西的生成器,一個空的函數不起作用;函數需要一個 yield 語句才能被識別為生成器。

<?php

function gndn()
{
}

foreach(
gndn() as $it)
{
echo
'FNORD';
}

?>

但是,即使無法到達,只要在語法上存在 yield 就足夠了

<?php

function gndn()
{
if(
false) { yield; }
}

foreach(
gndn() as $it)
{
echo
'FNORD';
}

?>
zilvinas at kuusas dot lt
9 年前
不要直接呼叫產生器函式,那樣不會work。

<?php

function my_transform($value) {
var_dump($value);
return
$value * 2;
}

function
my_function(array $values) {
foreach (
$values as $value) {
yield
my_transform($value);
}
}

$data = [1, 5, 10];
// my_transform() 不會在 my_function() 內部被呼叫
my_function($data);

# my_transform() 將會被呼叫。
foreach (my_function($data) as $val) {
// ...
}
?>
Harun Yasar harunyasar at mail dot com
9 年前
這是一個簡單的費波那契產生器。

<?php
function fibonacci($item) {
$a = 0;
$b = 1;
for (
$i = 0; $i < $item; $i++) {
yield
$a;
$a = $b - $a;
$b = $a + $b;
}
}

# 給我前十個費波那契數
$fibo = fibonacci(10);
foreach (
$fibo as $value) {
echo
"$value\n";
}
?>
christophe dot maymard at gmail dot com
10 年前
<?php
//使用產生器實作 IteratorAggregate 介面的類別範例

class ValueCollection implements IteratorAggregate
{
private
$items = array();

public function
addValue($item)
{
$this->items[] = $item;
return
$this;
}

public function
getIterator()
{
foreach (
$this->items as $item) {
yield
$item;
}
}
}

//初始化一個集合
$collection = new ValueCollection();
$collection
->addValue('A string')
->
addValue(new stdClass())
->
addValue(NULL);

foreach (
$collection as $item) {
var_dump($item);
}
Shumeyko Dmitriy
11 年前
這是使用產生器進行遞迴的小範例。使用的 PHP 版本是 5.5.5
[php]
<?php
define
("DS", DIRECTORY_SEPARATOR);
define ("ZERO_DEPTH", 0);
define ("DEPTHLESS", -1);
define ("OPEN_SUCCESS", True);
define ("END_OF_LIST", False);
define ("CURRENT_DIR", ".");
define ("PARENT_DIR", "..");

function
DirTreeTraversal($DirName, $MaxDepth = DEPTHLESS, $CurrDepth = ZERO_DEPTH)
{
if ((
$MaxDepth === DEPTHLESS) || ($CurrDepth < $MaxDepth)) {
$DirHandle = opendir($DirName);
if (
$DirHandle !== OPEN_SUCCESS) {
try{
while ((
$FileName = readdir($DirHandle)) !== END_OF_LIST) { //讀取目錄中的所有檔案
if (($FileName != CURRENT_DIR) && ($FileName != PARENT_DIR)) {
$FullName = $DirName.$FileName;
yield
$FullName;
if(
is_dir($FullName)) { //包含子檔案和目錄
$SubTrav = DirTreeTraversal($FullName.DS, $MaxDepth, ($CurrDepth + 1));
foreach(
$SubTrav as $SubItem) yield $SubItem;
}
}
}
} finally {
closedir($DirHandle);
}
}
}
}

$PathTrav = DirTreeTraversal("C:".DS, 2);
print
"<pre>";
foreach(
$PathTrav as $FileName) printf("%s\n", $FileName);
print
"</pre>";
[/
php]
harl at gmail dot com
4 個月前
如果您將帶有鍵的產生值與不帶鍵的產生值混合使用,結果與將值新增到帶有或不帶有鍵的陣列相同。

<?php
function gen() {
yield
'a';
yield
4 => 'b';
yield
'c';
}

$t = iterator_to_array(gen());
var_dump($t);
?>

結果是一個陣列 [0 => 'a', 4 => 'b', 5 => 'c'],就像您寫的一樣

<?php
$t
= [];
$t[] = 'a';
$t[4] = 'b';
$t[] = 'c';
var_dump($t);
?>

給予 'c' 的鍵會從上一個數字索引遞增。
christianggimenez at gmail dot com
5 年前
從 1 到 100 的數字的模數列表。

<?php

function list_of_modulo(){

for(
$i = 1; $i <= 100; $i++){

if((
$i % 2) == 0){
yield
$i;
}
}
}

$modulos = list_of_modulo();

foreach(
$modulos as $value){

echo
"$value\n";
}

?>
To Top