迭代器和生成器
一道面试题
如果让你用 PHP 生成从 1 到100 万个数值,请问怎么做才能最省内存?
没错,这是一道面试题,如果让你写出答案,你会有什么样的思路呢?请先独自思考几分钟。
可能你想到的会是这种方式:
1 |
|
你如果给出这种答案的话,肯定不会让面试官满意的。因为这道题的重点是节省内存,如果是如上的程序,虽然能够实现题目的前半部分要求,但实际运行起来却是一个非常吃内存的老虎。因为从 PHP 底层分析, makeLargeArray()
会预先创建一个由一百万个整数组成的数组分配内存!
那么到底要怎么做才能既节省内存,又能实现这道题的目标呢?
正确答案是,使用 PHP生成器 ,代码如下:
1 |
|
很少用 PHP 生成器的同学是不是看不太懂?没关系,本文才刚刚开始。
PHP 迭代器
在讲 PHP 生成器之前,我觉得有必要给大家讲一讲迭代器的概念。如果你对 PHP 迭代器的概念很了解,可以直接跳过这个章节,直接前往文中下半部分。
迭代是指反复执行一个过程,每执行一次叫做一次迭代。这么说你可能不是很理解,事实上,我们每天都在和迭代打交道,就比如 PHP 的 foreach()
函数,像这样:
1 |
|
上面一个简单的 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 字节的内存。
1 |
|
用法
我们需要注意的关键是 yield
,这是生成器的关键。我们通过上面例子,可以看得出,yield
会将当前一个值传递给 foreach
,换句话说, foreach
每一次迭代过程都会从 yield
处取一个值,直到整个遍历过程不再存在 yield
为止的时候,遍历结束。
我们也可以发现, yield
和 return
都会返回值,但区别在于一个 return
是返回既定结果,一次返回完毕就不再返回新的结果,而 yield
是 不断产出 直到无法产出为止。
实际上存在 yield
的函数返回值返回的是一个 Generator
对象(这个对象不能手动通过 new 实例化),该对象实现了 Iterator
接口。
你可能觉得以上例子没啥实际用途,我再举一个比较有用的例子(来自 Modern PHP 一书),比如我想导入一个大小约为 4GB 的 CSV 文件(你可以理解为 Excel),而且我们的服务器运行在一个共享的 VPS 中,只提供了 1 GB 的内存,所以不能直接把 CSV 这个生成的数组直接都放在内存里。那我们怎么用生成器+迭代器来实现呢?代码如下:
1 | // 生成器产出 |
导入导出这种需求在现实场景中是不是很常见,大家可以在项目里尝试使用生成器来取代传统的数组声明,可以帮公司省掉一大笔内存购买费用~
总结
生成器是功能多样性和简洁性之间的折中方案。生成器是只能向前进的迭代器,这意味着不能使用生成器在数据集中执行后退、快进或査找操作,只能让生成器计算并产生下一个值。迭代大型数据集或数列时最适合使用生成器,因为这样占用的系统内存量极小。生成器也能完成迭代器能完成的简单任务,而且使用的代码较少。
原文链接:述迭代器和生成器