[精选] 一起来用用 php的生成器 yield

如果是做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 个元素来获取一下内存使用量,这意味着我们将会提出四个检查点:

<?php
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 同样的例子:

<?php
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()    继续往下执行生成器
php
本作品采用《CC 协议》,转载必须注明作者和本文链接
不乱于心,不困于情,不畏将来,不念过往,如此,安好!
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3
sanders

既然讲到 yield 但我没有在文章中搜索到 from 关键字。我就多提一下 yield from 语法。

yield from 要解决的问题

我们在使用 yield 做迭代逻辑的时候,是否有碰到过封装问题?比如:代码结构封装好好的,但是想用 yield 迭代出调用了好几层函数中的内容,怎么办?在想办法构造协程之前,我们可以尝试官方提供的 yield from 语法,yield from 右侧跟一个 \Generator 实例,即组成表达式。

function transformData ()
{
    $hugeTask = glob(storage_path('/download/some_data/*.txt'));
    foreach ($hugeTask as $file) {
        yield from parseFile($file);
    }
}

function parseFile ($file)
{
    $fp = fopen($file,'r');
    while(!feof($fp)) {
        yield fgets($fp,4096);
    }
    fclose($fp);
}

function transformRow($line)
{
    // 处理单行数据
    return $line;
}

foreach (`transformData()  as $row) {
    // 这里可以处理每一行的数据
}

从这个例子上体现出, yield from 最大的意义就是在函数的调用过程中彼此传递 \Generator。具有良好编码风格的我们都会尽力不让这三个函数的代码融合在一个函数里,如果我们想通过在调用 transformData() 使用 foreach 处理所有文件内所有的行时,我们就需要在中途用到 yield from 语法。

需要注意的特性

那么 yield from 有没有需要注意的特性哪?都写到这里了那必须有,yield from 每次调用都会让 foreach 的下标参数重新从零计数,这在我们迭代数组时不会遇到可能会有些违反“常识”。我们用 fast-excel 包的时候,一开始就没有注意这个问题,就造成了多表头内容导出,因为这个包会在每个下标为0的数据之前加入表头。解决也很简单,找个变量单独计数,在最终迭代的位置使用 yield $this->offset => $row 进行处理就好了。

1个月前 评论

文章通俗易懂,有逻辑有测试,希望可以多写写yield更深层次的用法,之前看过Laruence的在PHP中使用协程实现多任务调度等,复看很多遍还是很难理解,小伙伴有兴趣的看看,记得回来分享下,www.laruence.com/2015/05/28/3038.h...

1个月前 评论

这个yield写的比较容易理解 :+1: :+1: :+1:

1个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!