我最近无意中发现了这段代码:
function xrange($min, $max)
{
for ($i = $min; $i <= $max; $i++) {
yield $i;
}
}
我以前从未见过这个yield关键字。试着运行我得到的代码
解析错误:语法错误,意外的T_VARIABLE在x行
那么yield关键字是什么呢?它是有效的PHP吗?如果是,我该怎么用呢?
我最近无意中发现了这段代码:
function xrange($min, $max)
{
for ($i = $min; $i <= $max; $i++) {
yield $i;
}
}
我以前从未见过这个yield关键字。试着运行我得到的代码
解析错误:语法错误,意外的T_VARIABLE在x行
那么yield关键字是什么呢?它是有效的PHP吗?如果是,我该怎么用呢?
当前回答
下面的代码说明了如何使用生成器在完成之前返回一个结果,而不像传统的非生成器方法在完整迭代之后返回一个完整的数组。使用下面的生成器,值在准备就绪时返回,不需要等待数组被完全填充:
<?php
function sleepiterate($length) {
for ($i=0; $i < $length; $i++) {
sleep(2);
yield $i;
}
}
foreach (sleepiterate(5) as $i) {
echo $i, PHP_EOL;
}
其他回答
简单的例子
<?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#
没有一个答案给出了使用由非数字成员填充的大型数组的具体示例。下面是一个例子,在一个大的.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在第二个示例中)。
什么是产量?
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多任务处理 生成器和数组的区别是什么? 关于生成器的维基百科
在实现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
一个值得在此讨论的有趣方面是参照让步。每次我们需要改变一个形参以使其反映在函数外部时,我们必须通过引用传递这个形参。要将此应用于生成器,只需在生成器的名称和迭代中使用的变量前加上&:
<?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变量的引用。