我最近偶然发现了这段代码:
function xrange($min, $max)
{
for ($i = $min; $i <= $max; $i++) {
yield $i;
}
}
之前我从未见过这个yield
关键字。试图运行我得到的代码
解析错误:语法错误,第x行意外的T_VARIABLE
那么这个yield
关键字是什么?它甚至是有效的PHP吗?如果是,我该如何使用它?
答案 0 :(得分:283)
yield
? yield
关键字returns data from a generator function:
生成器函数的核心是yield关键字。在最简单的形式中,yield语句看起来很像一个return语句,除了不是停止执行函数并返回,而是为代码提供循环生成器的代码值并暂停生成器函数的执行。
生成器函数实际上是编写Iterator的更紧凑和有效的方法。它允许您定义一个函数(您的xrange
)计算并返回值,而您是looping over it:
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
function来实现该输出。对,你是对的。输出将是相同的。不同之处在于我们如何到达那里。
当我们使用range
PHP时,会执行它,在内存中创建整个数字数组,并将{em>整个数组的return
创建到foreach
循环中然后将检查它并输出值。换句话说,foreach
将对阵列本身进行操作。 range
功能和foreach
仅“通话”一次。把它想象成在邮件中获取包裹。送货员将递给您包裹并离开。然后你打开整个包装,取出那里的东西。
当我们使用生成器函数时,PHP将进入函数并执行它,直到它满足结束或yield
关键字。当它遇到yield
时,它将返回当时的值到外循环。然后它返回到生成器功能并从它产生的地方继续。由于您的xrange
拥有for
循环,因此会执行并生成,直到达到$max
为止。可以把它想象成foreach
和生成乒乓球的发电机。
显然,生成器可用于解决内存限制。根据您的环境,执行range(1, 1000000)
会使您的脚本致命,而使用生成器的同样可以正常工作。或者像维基百科所说:
因为生成器仅根据需要计算其产生的值,所以它们对于表示一次性计算昂贵或无法计算的序列非常有用。这包括例如无限序列和实时数据流。
发电机也应该非常快。但请记住,当我们谈论快速时,我们通常会谈论很少的数字。因此,在您现在运行并更改所有代码以使用生成器之前,请先进行基准测试,看看它有意义。
生成器的另一个用例是异步协同程序。 yield
关键字不仅会返回值,还会接受它们。有关详细信息,请参阅下面链接的两篇优秀博客文章。
yield
? PHP 5.5 中引入了生成器。尝试在该版本之前使用yield
将导致各种解析错误,具体取决于关键字后面的代码。因此,如果您从该代码中获得解析错误,请更新您的PHP。
答案 1 :(得分:24)
此函数使用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()
只返回generator和b()
只是一个简单的数组。你可以迭代这两个。
此外,第一个不分配完整数组,因此对内存要求较低。
答案 2 :(得分:20)
yield
关键字用于在PHP 5.5中定义“生成器”。
好的,那么什么是generator?
来自php.net:
生成器提供了一种简单的方法来实现简单的迭代器,而无需实现实现Iterator接口的类的开销或复杂性。
生成器允许您编写使用foreach迭代一组数据的代码,而无需在内存中构建数组,这可能会导致超出内存限制,或者需要相当长的处理时间来生成。相反,您可以编写一个生成器函数,它与普通函数相同,除了不是返回一次,生成器可以生成所需数量的次数,以便提供要迭代的值。
从这个地方:generators = generator,其他函数(只是一个简单的函数)= functions。
因此,它们在以下情况下很有用:
你需要做一些简单的事情(或简单的事情);
生成器比实现Iterator接口简单得多。另一方面,发电机的功能较少。 compare them
您需要生成大量数据 - 节省内存;
实际上为了节省内存,我们可以通过每次循环迭代的函数生成所需的数据,并且在迭代之后使用垃圾。所以这里的要点是 - 清晰的代码和可能的性能。看看哪个更适合您的需求。
您需要生成序列,这取决于中间值;
这是前一个想法的延伸。与函数相比,生成器可以使事情变得更容易。检查Fibonacci example,并尝试在没有生成器的情况下生成序列。在这种情况下,生成器也可以更快地工作,至少是因为将中间值存储在局部变量中;
您需要提高效果。
在某些情况下,它们可以更快地运行(参见上一个好处);
答案 3 :(得分:19)
简单示例
<?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#
答案 4 :(得分:12)
以上答案均未显示使用由非数字成员组成的大规模阵列的具体示例。这是一个使用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 )。
答案 5 :(得分:6)
使用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:在无限循环中使用生成器就像编写一个具有无限长度的闭包......
答案 6 :(得分:0)
一个值得关注的有趣方面是参考引用。每次我们需要更改参数以使其反映在函数外部时,我们都必须通过引用传递此参数。要将其应用于生成器,我们只需在生成器的名称和迭代中使用的变量前面加上一个&符号&
:
<?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
变量的引用。