我最近无意中发现了这段代码:

function xrange($min, $max) 
{
    for ($i = $min; $i <= $max; $i++) {
        yield $i;
    }
}

我以前从未见过这个yield关键字。试着运行我得到的代码

解析错误:语法错误,意外的T_VARIABLE在x行

那么yield关键字是什么呢?它是有效的PHP吗?如果是,我该怎么用呢?


什么是产量?

yield关键字从生成器函数返回数据:

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

什么是生成器函数?

生成器函数实际上是编写Iterator的一种更紧凑和高效的方式。它允许你定义一个函数(你的xrange),当你循环遍历它时,它将计算并返回值:

function xrange($min, $max) {
    for ($i = $min; $i <= $max; $i++) {
        yield $i;
    }
}

[…]

foreach (xrange(1, 10) as $key => $value) {
    echo "$key => $value", PHP_EOL;
}

这将创建以下输出:

0 => 1
1 => 2
…
9 => 10

你也可以在foreach中使用

yield $someKey => $someValue;

在生成器函数中,$someKey是你希望出现在$key中的任何值,而$someValue是$val中的值。在问题的例子中是$i。

与普通函数有什么不同?

现在您可能想知道为什么我们不简单地使用PHP的本机范围函数来实现输出。你说得对。输出是一样的。不同的是我们是如何到达那里的。

当我们使用range PHP时,会执行它,在内存中创建整个数组,并将整个数组返回给foreach循环,foreach循环将遍历它并输出值。换句话说,foreach将对数组本身进行操作。range函数和foreach只“交谈”一次。把它想象成在邮箱里收到一个包裹。送货员会把包裹递给你,然后离开。然后你打开整个包装,把里面的东西拿出来。

当我们使用生成器函数时,PHP将进入该函数并执行它,直到它满足end或yield关键字。当它遇到yield时,它会将当时的值返回给外部循环。然后它回到生成器函数,从它产生的地方继续。由于xrange持有一个for循环,它将执行并屈服,直到达到$max。把它想象成foreach和发电机打乒乓球。

我为什么需要这个?

显然,生成器可以用来解决内存限制。根据您的环境,执行范围(1,1000000)将导致脚本死亡,而使用生成器也可以正常工作。或者如维基百科所说:

由于生成器只在需要时计算其产生的值,因此它们对于表示代价昂贵或不可能立即计算的序列非常有用。其中包括无限序列和实时数据流。

发电机也应该是相当快的。但请记住,当我们谈论快速时,我们通常谈论的是非常小的数字。因此,在运行并更改所有代码以使用生成器之前,先做一个基准测试,看看哪里有意义。

生成器的另一个用例是异步协程。yield关键字不仅返回值,而且接受值。有关这方面的详细信息,请参阅下面链接的两篇出色的博客文章。

我什么时候可以用yield了?

在PHP 5.5中引入了生成器。在该版本之前尝试使用yield将导致各种解析错误,这取决于关键字后面的代码。因此,如果从代码中得到一个解析错误,请更新PHP。

资料来源和进一步阅读:

官方文档 原始RFC kelunik的博客:生成器介绍 ircmaxell的博客:生成器可以为您做什么 NikiC的博客:PHP中使用协程的协作多任务处理 协作式PHP多任务处理 生成器和数组的区别是什么? 关于生成器的维基百科


yield关键字用于定义PHP 5.5中的“生成器”。 好的,那么什么是发电机?

从php.net:

Generators provide an easy way to implement simple iterators without the overhead or complexity of implementing a class that implements the Iterator interface. A generator allows you to write code that uses foreach to iterate over a set of data without needing to build an array in memory, which may cause you to exceed a memory limit, or require a considerable amount of processing time to generate. Instead, you can write a generator function, which is the same as a normal function, except that instead of returning once, a generator can yield as many times as it needs to in order to provide the values to be iterated over.

从这里开始:发电机=发电机,其他函数(只是一个简单的函数)=函数。

因此,它们在以下情况下很有用:

you need to do things simple (or simple things); generator is really much simplier then implementing the Iterator interface. other hand is, ofcource, that generators are less functional. compare them. you need to generate BIG amounts of data - saving memory; actually to save memory we can just generate needed data via functions for every loop iteration, and after iteration utilize garbage. so here main points is - clear code and probably performance. see what is better for your needs. you need to generate sequence, which depends on intermediate values; this is extending of the previous thought. generators can make things easier in comparison with functions. check Fibonacci example, and try to make sequence without generator. Also generators can work faster is this case, at least because of storing intermediate values in local variables; you need to improve performance. they can work faster then functions in some cases (see previous benefit);


使用yield,您可以很容易地描述单个函数中多个任务之间的断点。就是这样,没什么特别的。

$closure = function ($injected1, $injected2, ...){
    $returned = array();
    //task1 on $injected1
    $returned[] = $returned1;
//I need a breakpoint here!!!!!!!!!!!!!!!!!!!!!!!!!
    //task2 on $injected2
    $returned[] = $returned2;
    //...
    return $returned;
};
$returned = $closure($injected1, $injected2, ...);

如果task1和task2高度相关,但你需要在它们之间设置一个断点来做其他事情:

处理数据库行之间的空闲内存 运行对下一个任务提供依赖关系的其他任务,但通过理解当前代码,这些任务是不相关的 执行异步调用并等待结果 等等……

生成器是最好的解决方案,因为你不需要把你的代码分割成许多闭包,或者与其他代码混合,或者使用回调等等……您只需使用yield来添加一个断点,如果准备好了,可以从该断点继续。

添加没有生成器的断点:

$closure1 = function ($injected1){
    //task1 on $injected1
    return $returned1;
};
$closure2 = function ($injected2){
    //task2 on $injected2
    return $returned1;
};
//...
$returned1 = $closure1($injected1);
//breakpoint between task1 and task2
$returned2 = $closure2($injected2);
//...

使用生成器添加断点

$closure = function (){
    $injected1 = yield;
    //task1 on $injected1
    $injected2 = (yield($returned1));
    //task2 on $injected2
    $injected3 = (yield($returned2));
    //...
    yield($returnedN);
};
$generator = $closure();
$returned1 = $generator->send($injected1);
//breakpoint between task1 and task2
$returned2 = $generator->send($injected2);
//...
$returnedN = $generator->send($injectedN);

注意:使用生成器很容易犯错误,所以在实现它们之前一定要编写单元测试! 注2:在无限循环中使用生成器就像写一个无限长的闭包……


简单的例子

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $v)
    echo $v.',';
echo '#end main#';
?>

输出

#start main# {start[1,2,3,4,5,6,7,8,9,]end} #end main#

先进的例子

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $k => $v){
    if($k === 5)
        break;
    echo $k.'=>'.$v.',';
}
echo '#end main#';
?>

输出

#start main# {start[0=>1,1=>2,2=>3,3=>4,4=>5,#end main#

这个函数使用yield:

function a($items) {
    foreach ($items as $item) {
        yield $item + 1;
    }
}

它和这个几乎一样,没有:

function b($items) {
    $result = [];
    foreach ($items as $item) {
        $result[] = $item + 1;
    }
    return $result;
}

唯一的区别是a()返回一个生成器,而b()只是一个简单的数组。你可以在两者上进行迭代。

另外,第一个不分配完整的数组,因此内存需求更少。


一个值得在此讨论的有趣方面是参照让步。每次我们需要改变一个形参以使其反映在函数外部时,我们必须通过引用传递这个形参。要将此应用于生成器,只需在生成器的名称和迭代中使用的变量前加上&:

 <?php 
 /**
 * Yields by reference.
 * @param int $from
 */
function &counter($from) {
    while ($from > 0) {
        yield $from;
    }
}

foreach (counter(100) as &$value) {
    $value--;
    echo $value . '...';
}

// Output: 99...98...97...96...95...

上面的例子展示了在foreach循环中改变迭代值如何改变生成器中的$from变量。这是因为由于生成器名称前有&号,$from是通过引用产生的。正因为如此,foreach循环中的$value变量是生成器函数中$from变量的引用。


下面的代码说明了如何使用生成器在完成之前返回一个结果,而不像传统的非生成器方法在完整迭代之后返回一个完整的数组。使用下面的生成器,值在准备就绪时返回,不需要等待数组被完全填充:

<?php 

function sleepiterate($length) {
    for ($i=0; $i < $length; $i++) {
        sleep(2);
        yield $i;
    }
}

foreach (sleepiterate(5) as $i) {
    echo $i, PHP_EOL;
}

没有一个答案给出了使用由非数字成员填充的大型数组的具体示例。下面是一个例子,在一个大的.txt文件(在我的用例中是262MB)上使用由explosion()生成的数组:

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

$path = './file.txt';
$content = file_get_contents($path);

foreach(explode("\n", $content) as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

输出结果是:

Starting memory usage: 415160
Final memory usage: 270948256

现在将其与类似的脚本进行比较,使用yield关键字:

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

function x() {
    $path = './file.txt';
    $content = file_get_contents($path);
    foreach(explode("\n", $content) as $x) {
        yield $x;
    }
}

foreach(x() as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

这个脚本的输出是:

Starting memory usage: 415152
Final memory usage: 415616

显然,内存使用节省相当大(ΔMemoryUsage -----> ~270.5 MB在第一个示例中,~450B在第二个示例中)。


在实现PHP IteratorAggregate接口时,yield关键字将很有用。查看文档,有几个使用ArrayIterator或yield的例子。

另一个例子是php-ds/polyfill repo: https://github.com/php-ds/polyfill/blob/e52796c50aac6e6cfa6a0e8182943027bacbe187/src/Traits/GenericSequence.php#L359

这个想法类似于下面的例子:

class Collection implements \IteratorAggregate
{
    private $array = [];

    public function push(...$values)
    {
        array_push($this->array, ...$values);
    }

    public function getIterator()
    {
        foreach ($this->array as $value) {
            yield $value;
        }
    }
}

$collection = new Collection();
$collection->push('apple', 'orange', 'banana');

foreach ($collection as $key => $value) {
    echo sprintf("[%s] => %s\n", $key, $value);
}

输出:

[0] => apple
[1] => orange
[2] => banana