php生成器函数与yield关键字

初次接触迭代器与生成器是在Python中,之后了解到在 php5.5 中也引入了生成器的特性,
但很多PHP开发者或许都不知道生成器这个功能,可能是因为平时使用场景较少吧。
但是,生成器功能的确非常有用。

优点:

  • 生成器会对PHP应用的性能有非常大的影响
  • PHP代码运行时节省大量的内存
  • 比较适合计算大量的数据

使用一个简单的例子说明(迭代输出从1开始到10000的数组,步进为1):

<?php
$start_mem = memory_get_usage();
$arr = range( 1, 10000 );
foreach( $arr as $value ){
    //echo $value.',';
}
$end_mem = memory_get_usage();
echo " use mem : ". ( $end_mem - $start_mem ) .'bytes'.PHP_EOL;

输入结果: 冯奎博客

<?php
$start_mem = memory_get_usage();
function xrange($start, $limit, $step = 1) {
    for ($i = $start; $i <= $limit; $i += $step) {
        // 注意变量$i的值在不同的yield之间是保持传递的。
        yield $i;
    }
}
foreach( xrange( 0, 10000 ) as $value ){
    echo $value.PHP_EOL;
}
$end_mem = memory_get_usage();
echo " use mem : ". ( $end_mem - $start_mem ) .'bytes'.PHP_EOL;

输入结果:冯奎博客

首先从运行结果上来看,528440bytes32bytes这两个内存消耗就一目了然了,
在生成器中提供了一种更容易的方法来实现简单的对象迭代(循环),
相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。


相关代码剖析

这里使用示例代码,在命令行下执行,加上 sleep(1) 可使执行结果更加明显

<?php
function xrange($start, $limit, $step = 1) {
    // echo '生成器开始执行' . PHP_EOL;
    for ($i = $start; $i <= $limit; $i += $step) {
        // echo '产生数据之前:' . $i  . PHP_EOL;
        yield $i;
        // echo '产生数据之后:' . $i  . PHP_EOL;
    }
    // echo '再来一个数据' . PHP_EOL;
    yield 100;
    // echo '生成器执行结束' . PHP_EOL;
}
$arr = xrange( 0, 5 );
// echo '生成器开始执行了吗?' . PHP_EOL;
sleep(1);
foreach( $arr as $value ){
    sleep(1);
    // echo '使用数据前' . PHP_EOL;
    echo '使用数据:' . $value . PHP_EOL;
    // echo '使用数据后' . PHP_EOL;
}

输出结果:

使用数据:0
使用数据:1
使用数据:2
使用数据:3
使用数据:4
使用数据:5
使用数据:100

我们可以看到数据在一行一行的输出,接着我们去掉代码中的注释,再次执行一遍
输出结果:

生成器开始执行了吗?
生成器开始执行
产生数据之前:0
使用数据前
使用数据:0
使用数据后
产生数据之后:0
产生数据之前:1
使用数据前
使用数据:1
使用数据后
产生数据之后:1
...
产生数据之前:5
使用数据前
使用数据:5
使用数据后
产生数据之后:5
再来一个数据
使用数据前
使用数据:100
使用数据后
生成器执行结束

还原一下代码执行过程:

  1. 首先调用 xrange 函数(生成器),传入( 0, 5 ),这里我们看到生成器并没有开始执行
  2. foreach 开始对 $arr 循环,执行生成器,接着 for 产生第一个数据,将数据 0 返回到 foreach 中,第一次 for 循环结束
  3. foreach 准备第二次循环,接着 for 产生第二个数据,将数据 1 返回到 foreach 中,第二次 foreach 循环结束,第二次 for 循环结束
  4. 这里 foreach 循环循环 6 次,for 循环六次,至此 for 循环结束
  5. foreach 循环第七次,输出生成器中最后一个数 100,到此 foreach 循环结束

从代码中我们看到,始终只有一个记录值参与循环,内存中也只有一条信息。
无论开始传入的 $arr 有多大,由于并不会立即生成所有结果集,所以内存始终是一条循环的值

生成器函数的核心 -- yield关键字

yield 最简单的调用形式看起来像一个 return 申明,不同之处在于普通 return 会返回值并终止函数的执行,yield 会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。


概念理解

到这里,你应该已经大概理解什么是生成器了。下面我们来说下生成器原理。

首先明确一个概念:生成器 yield 关键字不是返回值,他的专业术语叫产出值,只是生成一个值

那么代码中 foreach 循环的是什么?其实是PHP在使用生成器的时候,会返回一个 Generator 类的对象。foreach 可以对该对象进行迭代,每一次迭代,PHP会通过 Generator 实例计算出下一次需要迭代的值。这样 foreach 就知道下一次需要迭代的值了。

而且,在运行中 for 循环执行后,会立即停止。等待 foreach 下次循环时候再次和 for 索要下次的值的时候,for 循环才会再执行一次,然后立即再次停止。直到不满足条件不执行结束。

在下一篇文章中我们介绍一下生成器的具体使用

参考文章:

1、生成器总览生成器语法
2、PHP的yield是个什么玩意
3、PHP中被忽略的性能优化利器:生成器

冯奎博客
请先登录后发表评论
  • latest comments
  • 总共0条评论