在PHP中yield是什么意思?


232

我最近偶然发现了以下代码:

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

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

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

那么这个yield关键字是什么呢?它甚至是有效的PHP吗?如果是,该如何使用?

Answers:


355

什么yield

yield关键字从发电机函数返回数据:

生成器函数的核心是yield关键字。以最简单的形式,yield语句看起来很像return语句,除了yield而不是停止函数的执行并返回,而是为循环生成器的代码提供一个值,并暂停生成器函数的执行。

什么是生成器函数?

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

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

这将创建以下输出:

0 => 1
1 => 2

9 => 10

您也可以使用来控制$key中的foreach

yield $someKey => $someValue;

在生成器函数中,$someKey是您想要显示的内容$key$someValue是中的值$val。在问题的例子中$i

与正常功能有何不同?

现在您可能想知道为什么我们不仅仅使用PHP的本机range函数来实现该输出。是的,你是。输出将是相同的。不同之处在于我们到达那里的方式。

当我们使用rangePHP,将执行它,在内存中创建一个数字的整个阵列,并return认为整个阵列foreach循环,然后将去在它和输出的值。换句话说,foreachwill将对数组本身进行操作。该range功能和foreach唯一的“通话”一次。可以将其想像为通过邮件获取包裹。送货员将把包裹交给您,然后离开。然后解开整个包装,取出里面的任何东西。

当我们使用生成器函数时,PHP将逐步进入该函数并执行,直到遇到结尾或yield关键字为止。当遇到a时yield,它将把当时的值返回外循环。然后,它返回到生成器函数,并从产生的地方继续。由于您xrange拥有一个for循环,它将执行并屈服直到$max达到。就像把它foreach和发生器打乒乓球一样。

我为什么需要那个?

显然,生成器可用于解决内存限制。根据您的环境,执行range(1, 1000000)遗嘱会使您的脚本致命,而对生成器执行同样的操作会很好。或如Wikipedia所述:

由于生成器仅按需计算其屈服值,因此它们对于表示昂贵或无法立即计算的序列很有用。这些包括例如无限序列和实时数据流。

发电机也应该很快。但是请记住,当我们谈论快速时,我们通常会谈论的次数很少。因此,在您开始运行并更改所有代码以使用生成器之前,请进行基准测试以了解在何处有意义。

生成器的另一个用例是异步协程。该yield关键字不仅返回值,但它也接受他们。有关此内容的详细信息,请参见下面的两个出色的博客文章链接。

从什么时候起可以使用yield

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

资料和进一步阅读:


1
请详细说明yeild这种解决方案带来的好处,例如:ideone.com/xgqevM
Mike

1
嗯,还有我正在生成的通知。嗯 好吧,我尝试使用辅助程序类对PHP> = 5.0.0的Generators进行仿真,是的,可读性稍差,但将来可能会用到它。有趣的话题。谢谢!
迈克

不是可读性而是内存使用率!比较用过的内存进行迭代return range(1,100000000)for ($i=0; $i<100000000; $i++) yield $i
emix

@mike是的,虽然我的回答中已经对此进行了解释。在另一个Mike的示例中,内存几乎不是问题,因为他仅迭代10个值。
戈登

1
@Mike xrange的一个问题是,它对静态限制的使用对于例如嵌套很有用(例如,在n维流形上搜索,或使用生成器进行递归快速排序)。您不能嵌套xrange循环,因为它的计数器只有一个实例。Yield版本不会遇到此问题。
谢恩2015年

43

该函数正在使用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()一个简单的数组。您可以在两者上进行迭代。

同样,第一个不分配完整的数组,因此对内存的需求较少。


2
官方文档的补充说明:在PHP 5中,生成器无法返回值:这样做将导致编译错误。空的return语句是生成器中的有效语法,它将终止生成器。从PHP 7.0开始,生成器可以返回值,可以使用Generator :: getReturn()进行检索。 php.net/manual/en/language.generators.syntax.php
程序员Dancuk

简单明了。
John Miller

24

简单的例子

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

那么,它返回而不中断功能吗?
Lucas Bustamante

22

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

从php.net:

生成器提供了一种简单的方法来实现简单的迭代器,而不会产生实现迭代器接口的类的开销或复杂性。

生成器允许您编写使用foreach遍历一组数据的代码,而无需在内存中构建数组,这可能会导致您超出内存限制,或需要大量的处理时间才能生成。取而代之的是,您可以编写一个生成器函数,该函数与普通函数相同,不同之处在于,生成器可以返回所需次数,而不是返回一次,以便提供要迭代的值。

从这里开始:generators =生成器,其他功能(仅仅是一个简单的功能)=功能。

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

  • 您需要做简单的事情(或简单的事情);

    generator实际上比实现Iterator接口要简单得多。另一方面,发电机的功能较差。比较他们

  • 您需要生成大量数据-节省内存;

    实际上,为了节省内存,我们只需要为每次循环迭代通过函数生成所需的数据,然后在迭代之后利用垃圾即可。所以这里要点是-清晰的代码和可能的性能。看看有什么更适合您的需求。

  • 您需要生成依赖于中间值的序列;

    这是先前思想的延伸。与函数相比,生成器可以使事情变得更容易。查看Fibonacci示例,并尝试在没有生成器的情况下进行排序。在这种情况下,生成器也可以更快地工作,至少是因为将中间值存储在局部变量中。

  • 您需要提高性能。

    在某些情况下,它们可以更快地工作,然后起作用(请参见先前的好处);


1
我不了解发电机的工作方式。此类实现迭代器接口。据我所知,迭代器类使我可以配置如何对对象进行迭代。例如ArrayIterator获取一个数组或对象,这样我就可以在迭代时修改值和键。因此,如果迭代器获取了整个对象/数组,那么生成器如何不必在内存中构建整个数组?
user3021621 2014年

7

有了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);

注意:使用生成器很容易出错,因此在实现它们之前,请务必先编写单元测试! note2:在无限循环中使用生成器就像编写一个无限长的闭包...


4

上面的答案均未显示使用由非数字成员组成的大规模数组的具体示例。这是一个使用explode()大.txt文件(在我的用例中为262MB)上生成的数组的示例:

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


3

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

 <?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通过参考产生由于发电机名称前的符号。因此,循环$value内的变量foreach$from对生成器函数内的变量的引用。


0

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

<?php 

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

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

因此,不可能使用yield在php中生成html代码吗?我不知道实际环境中的好处
Giuseppe Lodi Rizzini

@GiuseppeLodiRizzini是什么让您觉得呢?
布拉德·肯特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.