yield在PHP中是什么意思?

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

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

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

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

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

138411 次浏览

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中使用

yield $someKey => $someValue;

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

与普通函数有什么不同?

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

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

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

我为什么需要这个?

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

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

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

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

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

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

资料来源和进一步阅读:

yield关键字用于定义“生成器”;在PHP 5.5中。 好,那么发电机是什么?< / p >

从php.net:

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

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

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

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

  • < p > 你需要做简单的事情(或简单的事情);

    generator比实现Iterator接口简单得多。另一方面,当然,发电机的功能较弱。比较它们

  • < p > 你需要生成大量的数据-节省内存;

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

  • < p > 你需要生成序列,这取决于中间值;

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

  • < p > 你需要提高性能。

    在某些情况下,它们可以比函数工作得更快(见之前的好处);

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

没有一个答案给出了使用由非数字成员填充的大型数组的具体示例。下面是一个例子,使用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在第一个例子,~ 450 b在第二个例子)。

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

另一个例子是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