PHP 8.4.2 Released!

生成器语法

生成器函数看起来像普通函数——不同的是普通函数返回一个值,而生成器可以 yield 生成多个想要的值。 任何包含 yield 的函数都是一个生成器函数。

当一个生成器被调用的时候,它返回一个可以被遍历的对象.当你遍历这个对象的时候(例如通过一个foreach循环),PHP 将会在每次需要值的时候调用对象的遍历方法,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。

一旦不再需要产生更多的值,生成器可以简单返回,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。

注意:

生成器能够返回多个值,通过 Generator::getReturn() 可以获取到。

yield 关键字

生成器函数的核心是yield关键字。它最简单的调用形式看起来像一个return申明,不同之处在于普通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
/*
* 下面每一行是用分号分割的字段组合,第一个字段将被用作键名。
*/

$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

生成 null 值

Yield 可以在没有参数传入的情况下被调用来生成一个 null 值并配对一个自动的键名。

示例 #3 生成nulls

<?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 通过生成值。 外部生成器将从内部生成器、object、array 中生成所有的值,直到它们不再有效, 之后将在外部生成器中继续执行。

如果生成器与 yield from 一起使用,那么 yield from 表达式将返回内部生成器返回的任何值。

警告

存储到 array (例如使用 iterator_to_array()

yield from 不能重置 key。它保留 Traversable 对象或者 array 返回的 key。因此,某些值可能会与其他的 yield 或者 yield from 共享公共的 key,因此,在插入数组时将会用这个 key 覆盖以前的值。

一个非常重要的常见情况是 iterator_to_array() 默认返回带 key 的 array , 这可能会造成无法预料的结果。 iterator_to_array() 还有第二个参数 preserve_keys ,可以设置为 false 来收集 Generator 返回的不带 key 的所有值。

示例 #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 notes

up
124
Adil lhan (adilmedya at gmail dot com)
11 years ago
For example yield keyword with Fibonacci:

function getFibonacci()
{
$i = 0;
$k = 1; //first fibonacci value
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; // infinite loop prevent
}
}
up
51
info at boukeversteegh dot nl
9 years ago
[This comment replaces my previous comment]

You can use generators to do lazy loading of lists. You only compute the items that are actually used. However, when you want to load more items, how to cache the ones already loaded?

Here is how to do cached lazy loading with a generator:

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

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

# Second (items 1-5 are cached, 6-10 are loaded)
print "Second\n";
foreach(
$f->getItems() as $i) {
print
$i . "\n";
}

# Third (all items are cached and returned instantly)
print "Third\n";
foreach(
$f->getItems() as $i) {
print
$i . "\n";
}
?>
up
20
Hayley Watson
9 years ago
If for some strange reason you need a generator that doesn't yield anything, an empty function doesn't work; the function needs a yield statement to be recognised as a generator.

<?php

function gndn()
{
}

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

?>

But it's enough to have the yield syntactically present even if it's not reachable:

<?php

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

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

?>
up
13
zilvinas at kuusas dot lt
9 years ago
Do not call generator functions directly, that won't 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() won't be called inside my_function()
my_function($data);

# my_transform() will be called.
foreach (my_function($data) as $val) {
// ...
}
?>
up
13
Harun Yasar harunyasar at mail dot com
9 years ago
That is a simple fibonacci generator.

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

# give me the first ten fibonacci numbers
$fibo = fibonacci(10);
foreach (
$fibo as $value) {
echo
"$value\n";
}
?>
up
11
christophe dot maymard at gmail dot com
10 years ago
<?php
//Example of class implementing IteratorAggregate using generator

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

//Initializes a collection
$collection = new ValueCollection();
$collection
->addValue('A string')
->
addValue(new stdClass())
->
addValue(NULL);

foreach (
$collection as $item) {
var_dump($item);
}
up
7
Shumeyko Dmitriy
11 years ago
This is little example of using generators with recursion. Used version of php is 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) { //read all file in directory
if (($FileName != CURRENT_DIR) && ($FileName != PARENT_DIR)) {
$FullName = $DirName.$FileName;
yield
$FullName;
if(
is_dir($FullName)) { //include sub files and directories
$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]
up
1
harl at gmail dot com
4 months ago
If you mix yielding values with keys and yielding values without keys, the result is the same as adding values to an array with or without keys.

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

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

The result is an array [0 => 'a', 4 => 'b', 5 => 'c'], just as if you had written

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

With the key given to 'c' being incremented from the previous numeric index.
up
-1
christianggimenez at gmail dot com
5 years ago
Module list of a number from 1 to 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