迭代器和生成器

栏目: PHP · 发布时间: 6年前

内容简介:迭代器和生成器

如果让你用 PHP 生成从 1 到100 万个数值,请问怎么做才能最省内存?

没错,这是一道面试题,如果让你写出答案,你会有什么样的思路呢?请先独自思考几分钟。

可能你想到的会是这种方式:

<?php
// 我自己本地测试大概用了 34MB 内存
function makeLargeArray($length){
    $dataset = [];
    for ($i = 0; $i < $length; $i++) {
        $dataset[] = $i;
    }
    return $dataset;
}
$customRange = makeLargeArray(1000000);
foreach ($customRange as $i) {
    echo $i, PHP_EOL;
}

// 记录使用的内存
function formatBytes($bytes, $precision =2){
    $units = array("b", "kb", "mb", "gb", "tb");

    $bytes = max($bytes, 0);
    $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
    $pow = min($pow, count($units) - 1);

    $bytes /= (1 << (10 * $pow));

    return round($bytes, $precision) . " " . $units[$pow];
}

print formatBytes(memory_get_peak_usage());

你如果给出这种答案的话,肯定不会让面试官满意的。因为这道题的重点是节省内存,如果是如上的程序,虽然能够实现题目的前半部分要求,但实际运行起来却是一个非常吃内存的老虎。因为从 PHP 底层分析, makeLargeArray() 会预先创建一个由一百万个整数组成的数组分配内存!

那么到底要怎么做才能既节省内存,又能实现这道题的目标呢?

正确答案是,使用 PHP生成器 ,代码如下:

<?php
// 我本地测试大概用了 718KB 内存
function makeLargeArray($length){
    for ($i = 0; $i < $length; $i++) {
        yield $i;
    }
}
foreach (makeLargeArray(1000000) as $i) {
    echo $i, PHP_EOL;
}

很少用 PHP 生成器的同学是不是看不太懂?没关系,本文才刚刚开始。

PHP 迭代器

在讲 PHP 生成器之前,我觉得有必要给大家讲一讲迭代器的概念。如果你对 PHP 迭代器的概念很了解,可以直接跳过这个章节,直接前往文中下半部分。

迭代是指反复执行一个过程,每执行一次叫做一次迭代。这么说你可能不是很理解,事实上,我们每天都在和迭代打交道,就比如 PHP 的 foreach() 函数,像这样:

<?php
$home = [
    'bed',
    'television',
    'computer'
];
foreach ($home as $furniture) {
    echo 'There is a '.$furniture.'in my home!';
}

上面一个简单的 foreach() 就是一个迭代器,它将 home 一次又一次的遍历,输出三个家具 furniture 。实际上,发生变化的是 home 这个数组,你可以把它当做是一个对象, foreach 在每次遍历它时,都会调用这个对象里的一个方法,让数组在自己内部的指针发生一次变化(迭代)。

所以,我们可以称 home 数组为 迭代器对象,而 foreach 就是一个 迭代器接口( Iterator )。

PHP 生成器

概念

通过上面的例子,想必大家对生成器有一个大概的理解,先来看看官方的解释(慢点儿度读,好好理解):生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。

生成器( Generator )是 PHP5.5 引入的功能,往往没有被大家充分利用。其实这是一个非常有用的功能,我相信大多数开发者和我一样不知道有生成器这种东西,因为平时工作中不常用,其实很简单,生成器就是迭代器,仅此而已。

作用

生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。

生成器的用法

我觉得除了我给的第一个例子,官方给的用例是也能很好的解释生成器的益处。比如 PHP 的一个函数:range ,它可以建立一个包含指定范围单元的数组,标准的 range() 函数需要在内存中生成一个数组包含每一个在它范围内的值,然后返回该数组, 结果就是会产生多个很大的数组。 比如,调用 range(0, 1000000) 将导致内存占用超过 100 MB

做为一种替代方法, 我们可以实现一个 xrange() 生成器, 只需要足够的内存来创建 Iterator 对象并在内部跟踪生成器的当前状态,这样只需要不到 1K 字节的内存。

<?php
function xrange($start, $limit, $step =1){
    if ($start < $limit) {
        if ($step <= 0) {
            throw new LogicException('Step must be +ve');
        }

        for ($i = $start; $i <= $limit; $i += $step) {
            yield $i;
        }
    } else {
        if ($step >= 0) {
            throw new LogicException('Step must be -ve');
        }

        for ($i = $start; $i >= $limit; $i += $step) {
            yield $i;
        }
    }
}
/* 注意下面range()和xrange()输出的结果是一样的。 */
echo 'Single digit odd numbers from range(): ';
foreach (range(1, 9, 2) as $number) {
    echo "$number ";
}
echo "\n";
echo 'Single digit odd numbers from xrange(): ';
foreach (xrange(1, 9, 2) as $number) {
    echo "$number ";
}
// 以上例程会输出:
Single digit odd numbers from range():  1 3 5 7 9
Single digit odd numbers from xrange(): 1 3 5 7 9
?>

用法

我们需要注意的关键是 yield ,这是生成器的关键。我们通过上面例子,可以看得出, yield 会将当前一个值传递给 foreach ,换句话说, foreach 每一次迭代过程都会从 yield 处取一个值,直到整个遍历过程不再存在 yield 为止的时候,遍历结束。

我们也可以发现, yieldreturn 都会返回值,但区别在于一个 return 是返回既定结果,一次返回完毕就不再返回新的结果,而 yield 是 不断产出 直到无法产出为止。

实际上存在 yield 的函数返回值返回的是一个 Generator 对象(这个对象不能手动通过 new 实例化),该对象实现了 Iterator 接口。

你可能觉得以上例子没啥实际用途,我再举一个比较有用的例子(来自 Modern PHP 一书),比如我想导入一个大小约为 4GB 的 CSV 文件(你可以理解为 Excel),而且我们的服务器运行在一个共享的 VPS 中,只提供了 1 GB 的内存,所以不能直接把 CSV 这个生成的数组直接都放在内存里。那我们怎么用生成器+迭代器来实现呢?代码如下:

// 生成器产出
function getRows($file){
    $handel = fopen($file, 'rb');
    if ($handel === false) {
        throw new Exception("Error Processing", 1);
    }
    while (feof($handel) === false) {
        yield fgetcsv($handel);
    }
    fclose($handel);
}
// 迭代器读取
foreach (getRows('./data.csv') as $row) {
    print_r($row);
}

导入导出这种需求在现实场景中是不是很常见,大家可以在项目里尝试使用生成器来取代传统的数组声明,可以帮公司省掉一大笔内存购买费用~


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

从零开始做运营

从零开始做运营

张亮 / 中信出版社 / 2015-11-1 / 49.00元

运营是什么?怎样做运营?产品和运营是什么关系?我是否适合从事互联网运营?为什么我做的运营活动收效甚微? 在互联网大热的今天,互联网运营成为一个越来越重要的岗位,事关网站、产品的发展与存亡。很多年轻人带着对互联网的热情投身到这个行业,却发现自己对这个行业所知甚少,对互联网运营更加陌生,甚至有一些有志于从事互联网运营的人,因为对运营缺乏了解而难以确定自己的职业发展方向。本书的出发点就在于此,它将......一起来看看 《从零开始做运营》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具