首页 / php开发 / 一起来用用 php的生成器 yield

一起来用用 php的生成器 yield

2022-07-15 07:15 php开发 阅读 2138 来源

如果是做Python或者其他语言的小伙伴,对于生成器应该不陌生。但很多PHP开发者或许都不知道生成器这个功能,可能是因为生成器是PHP 5.5.0才引入的功能,也可以是生成器作用不是很明显。但是,生成器功能的确非常有用。

1. 什么是 "yield"

生成器函数看上去就像一个普通函数, 除了不是返回一个值之外, 生成器会根据需求产生更多的值。

看以下的例子:

function getValues() {
    yield 'value';
}
// 输出字符串 "value"
echo getValues();

当然, 这不是他生效的方式, 前面的例子会给你一个致命的错误: 类生成器的对象不能被转换成字符串

2 yield 解决的问题

解决运行内存的瓶颈,php程序中的变量存储在内存中,之前有遇到过读取Excel文件时候,会出现内存不足,出现:

Fatal Error: Allowed memory size of xxxxxx bytes 

所以会设置php 最大运行内存的设置: ini_set('memory_limit', '200M')

但是当我们读取5g 这么大的文件的时候,我们运行内存可能就吃不消了,所以会选择yield

3 "yield" & "return" 的区别

前面的错误意味着 getValues() 方法不会如预期返回一个字符串,让我们检查一下他的类型:

function getValues() {
    return 'value';
}
var_dump(getValues()); // string(5) "value"

function getValues() {
    yield 'value';
}
var_dump(getValues()); // class Generator#1 (0) {}

生成器 类实现了 生成器 接口, 这意味着你必须遍历 getValue() 方法来取值:

foreach (getValues() as $value) {
   echo $value;
}

// 使用变量也是好的
$values = getValues();
foreach ($values as $value) {
   echo $value;
}

但这不是唯一的不同!

一个生成器运行你写使用循环来迭代一维数组的代码,而不需要在内存中创建是一个数组,这可能会导致你超出内存限制。

在下面的例子里我们创建一个有 800,000 元素的数字同时从 getValues() 方法中返回他,同时在此期间,我们将使用函数 memory_get_usage() 来获取分配给次脚本的内存, 我们将会每增加 200,000 个元素来获取一下内存使用量,这意味着我们将会提出四个检查点:
function getValues() {
   $valuesArray = [];
   // 获取初始内存使用量
   echo round(memory_get_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
   for ($i = 1; $i < 800000; $i++) {
      $valuesArray[] = $i;
      // 为了让我们能进行分析,所以我们测量一下内存使用量
      if (($i % 200000) == 0) {
         // 来 MB 为单位获取内存使用量
         echo round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL;
      }
   }
   return $valuesArray;
}
$myValues = getValues(); // 一旦我们调用函数将会在这里创建数组
foreach ($myValues as $value) {}

前面例子发生的情况是这个脚本的内存消耗和输出:

0.34 MB
8.35 MB
16.35 MB
32.35 MB

这意味着我们的几行脚本消耗了超过 30 MB 的内存, 每次你你添加一个元素到 $valuesArray 数组中, 都会增加他在内存中的大小。

让我们使用 yield 同样的例子:
function getValues() {
   // 获取内存使用数据
   echo round(memory_get_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
   for ($i = 1; $i < 800000; $i++) {
      yield $i;
      // 做性能分析,因此可测量内存使用率
      if (($i % 200000) == 0) {
         // 内存使用以 MB 为单位
         echo round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL;
      }
   }
}
$myValues = getValues(); // 在循环之前都不会有动作
foreach ($myValues as $value) {} // 开始生成数据

这个脚本的输出令人惊讶:

0.34 MB
0.34 MB
0.34 MB
0.34 MB

这不意味着你从 return 表达式迁移到 yield,但如果你在应用中创建会导致服务器上内存出问题的巨大数组,则 yield 更加适合你的情况。

4. 什么是 "yield" 选项

这里有很多 yield 的选项, 我将强调他们中的几个:

a. 使用 yield, 你也可以使用 return

function getValues() {
   yield 'value';
   return 'returnValue';
}
$values = getValues();
foreach ($values as $value) {}
echo $values->getReturn(); // 'returnValue'

b. 返回键值对:

function getValues() {
   yield 'key' => 'value';
}
$values = getValues();
foreach ($values as $key => $value) {
   echo $key . ' => ' . $value;
}

5. 生成器

1 使用生成器来生成一个1到100的数组

function  my_range($start,$limit){    
    for($i=$start;$i<=$limit;$i++){
        yield $i;
    }
}

2 打印出来,看下返回究竟是什么

$arr = my_range(1,100);
var_dump($arr);

结果是:

object(Generator)#1 (0) {
}

可见是一个对象,是一个生成器对象,既然是对象那么也就是可以用foreach来遍历

3 遍历生成器

foreach($arr as $num){
    echo $num.PHP_EOL;
}
看到可以完整遍历出来,那么与那样实现的不同地方,意义在哪里呢。重点来了。

4 两者内存占用比较
上面已经测试过使用数组的方式,随着范围的增大占用的内存剧增,很快就超过了PHP的内存上限。

那么使用生成器占用了多少内存呢,来看看就知道了。

$start = memory_get_usage();
$arr   = my_range(1, 100);
$end   = memory_get_usage();
echo $end - $start .' bytes'.PHP_EOL;

可以看到只占用了576bytes,当然每个人测试的可能都会有点不同,环境不同,但是这不是重点。

我们再尝试增加数字范围,可以看到数字范围并没有影响到内存占用,也就是可以轻松的遍历超大数字。

$start = memory_get_usage();
$arr   = my_range(1, 100000000);
$end   = memory_get_usage();
echo $end - $start .' bytes'.PHP_EOL;

foreach($arr as $num){
    echo $num.PHP_EOL;
}

这下我们就可以遍历1到10000000的数字了,不相信内存占用那么低的小伙伴,可以打开任务管理器毫无波澜,即时再上调数字范围。

5、生成器遍历原理
生成器既然这么强大,那么他的遍历原理是什么呢。使用foreach遍历的时候,相当于生成器执行了以下操作。

while($arr->valid()){
    echo $arr->current().PHP_EOL;
    $arr->next();
}
//$arr->valid()     判断生成器是否关闭
//$arr->current() 返回当前对象
//$arr->next()    继续往下执行生成器

猜你喜欢