收益率在 PHP 中是什么意思



我最近偶然发现了这段代码:

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

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

解析错误:语法错误、第 x 行出现意外T_VARIABLE

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

什么是yield

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

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

什么是生成器功能?

生成器函数实际上是编写迭代器的一种更紧凑、更高效的方法。它允许您定义一个函数(您的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 $key使用

yield $someKey => $someValue;

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

请注意,在内部,顺序整数键与生成的值配对,就像与非关联数组配对一样。我们甚至可以使用键设置产量值。

与正常功能有什么区别?

现在你可能想知道为什么我们不简单地使用 PHP 的原生 range 函数来实现该输出。你是对的。输出将是相同的。区别在于我们如何到达那里。

当我们使用 PHP 时range将执行它,在内存中创建整个数字数组并将整个数组returnforeach循环,然后循环将遍历它并输出值。换句话说,foreach将对数组本身进行操作。range功能和foreach只"说话"一次。把它想象成在邮件中收到一个包裹。送货员会把包裹交给你然后离开。然后你打开整个包装,取出里面的任何东西。

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

我为什么需要它?

显然,生成器可用于解决内存限制。根据您的环境,执行range(1, 1000000)会使脚本致命,而生成器也是如此。或者正如维基百科所说:

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

发电机也应该非常快。但请记住,当我们谈论快速时,我们通常谈论的数量非常少。因此,在你现在跑掉并更改所有代码以使用生成器之前,请做一个基准测试,看看它在哪里有意义。

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

从什么时候开始可以使用yield

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

来源和进一步阅读:

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

此函数使用 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
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#

没有一个答案显示了使用由非数字成员填充的大量数组的具体示例。下面是一个使用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(。

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

从 php.net:

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

生成器允许您编写使用 foreach 迭代一组数据的代码,而无需在内存中构建数组,这可能会导致您超出内存限制,或者需要相当长的处理时间来生成。相反,您可以编写一个生成器函数,该函数与普通函数相同,只是生成器可以根据需要生成任意次数,而不是返回一次,以便提供要迭代的值。

从这个地方:生成器=生成器,其他函数(只是一个简单的函数(=函数。

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

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

    生成器实际上比实现迭代器接口要简单得多。 另一方面,Ofcource,发电机的功能较差。 比较它们。

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

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

  • 您需要生成序列,这取决于中间值;

    这是之前思想的延伸。 与函数相比,生成器可以使事情变得更容易。 检查斐波那契示例,并尝试在没有生成器的情况下制作序列。在这种情况下,生成器也可以更快地工作,至少是因为在局部变量中存储了中间值;

  • 您需要提高性能。

    在某些情况下,它们可以比功能更快地工作(请参阅之前的好处(;

使用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:在无限循环中使用生成器就像编写一个无限长度的闭包......

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

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

当实现 PHP IteratorAggregate 接口时,yield 关键字将很有用。查看文档,有几个示例使用 ArrayIteratoryield .

另一个例子是在php-ds/polyfill存储库中找到: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] => %sn", $key, $value);
}

输出:

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

相关内容

  • 没有找到相关文章

最新更新