PHP Conference Japan 2024

生成器语法

生成器函数看起来就像普通函数一样,只是它不是返回一个值,而是根据需要yield多个值。任何包含yield的函数都是生成器函数。

当调用生成器函数时,它会返回一个可以迭代的对象。当您迭代该对象(例如,通过foreach循环)时,PHP 将在每次需要值时调用该对象的迭代方法,然后在生成器生成值时保存生成器状态,以便在需要下一个值时恢复。

一旦没有更多值需要生成,生成器就可以简单地返回,调用代码继续执行,就像数组用完值一样。

注意:

生成器可以返回值,可以使用Generator::getReturn()检索。

yield 关键字

yield 关键字是生成器函数的核心。在其最简单的形式中,yield 语句看起来很像 return 语句,只是它不是停止函数执行并返回,而是将值提供给遍历生成器的循环代码并暂停生成器函数的执行。

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

生成空值

可以在没有参数的情况下调用 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 使用 yield fromiterator_to_array()

<?php
function inner() {
yield
1; // 键 0
yield 2; // 键 1
yield 3; // 键 2
}
function
gen() {
yield
0; // 键 0
yield from inner(); // 键 0-2
yield 4; // 键 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 "第一次\n";
foreach(
$f->getItems() as $i) {
print
$i . "\n";
if(
$i == 5 ) {
break;
}
}

# 第二次(项目 1-5 已缓存,6-10 已加载)
print "第二次\n";
foreach(
$f->getItems() as $i) {
print
$i . "\n";
}

# 第三次(所有项目都已缓存并立即返回)
print "第三次\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 年前
不要直接调用生成器函数,这样不起作用。



<?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 个月前
如果混合使用带键值和不带键值的 yield,结果与使用或不使用键值向数组添加值相同。

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