ModernPHP 生成器

本文介绍PHP中生成器的概念,以及它的应用。

0x00 简介

PHP生成器是PHP5.5.0引入的功能,生成器就是简单的迭代器,但与标准的PHP迭代器不同,PHP生成器不要求实现Iterator接口,从而减轻了类的负担。生成器会根据需求计算并产出要迭代的值,这对应用性能的影响重大。假如标准的PHP迭代器经常在内存中执行迭代操作,这要预先计算出数据集,性能低下;如果要使用特定的方式计算大量数据,对性能的影响更甚。此时我们使用生成器,即时计算出产出后续值,不占用宝贵的内存资源。

PHP生成器不能满足所有迭代操作的需求,因为如果不查询,生成器永远不知道下一个要迭代的值是什么,在生成器中无法快进或后退。生成器还是一次性的,无法多次迭代同一个生成器。不过,如果需要,可以重建或克隆生成器。

0x01 生成器的创建

生成器的创建很简单,因为生成器就是PHP函数,只不过要在函数中一次或多次使用yield关键字。与普通的PHP函数不同的是,生成器从不返回值,只产出值。

1
2
3
4
5
function myGenerator() {
yield 'value1';
yield 'value2';
yield 'value3';
}

调用生成器函数时,PHP会返回一个属于Generator类的对象。这个对象可以使用foreach()函数迭代。每次迭代,PHP会要求Generator实例计算并提供下一个要迭代的值。生成器的优雅体现在,每次产出一个值后,生成器的内部状态会停顿;向生成器请求下一个值时,内部状态又会恢复。

1
2
3
4
5
6
7
8
foreach ($myGenerator as $yieldedValue) {
echo $yieldedValue, PHP_EOL;
}

// 输出:
// value1
// value2
// value3

0x02 生成器的使用

下面我们来实现一个简单的函数,用于生成一个大范围内数值集合。

一个普通的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function makeRange($length) {
$dataset = [];
for($i=0; $i<$length; $i++) {
$dataset[] = $i;
}

return $dataset;
}

$customRange = makeRange(1000000);
foreach ($customRange as $i) {
echo $i, PHP_EOL;
}

上述的代码没有善用内存,因为makeRange()函数要为预先创建的一个有一百万个整数组成的数组分配内存。PHP生成器实现相同的操作。不过一个只会为一个整数分配内存。

使用生成器的栗子:

1
2
3
4
5
6
7
8
9
function makeRange($length) {
for($i=0; $i<$length; $i++) {
yield $i;
}
}

foreach (makeRange(1000000) as $i) {
echo $i, PHP_EOL;
}

这只是一个虚构的例子,在现实中,你可以想象一下使用生成器可以计算什么样的数据集。假设我们现在想迭代一个4GB的文本文件(例如黑客字典),而你的内存可能比4GB还小,这时候就不能把整个文件加载到内存中,下面使用生成器来实现读取一个cvs文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getRows($file) {
$handle = fopen($file, 'rb');
if($handle === false) {
throw new Exception();
}
while(feof($handle) === false) {
yield fgetcsv($handle);
}
fclose($handle);
}

foreach (getRows('data.csv') as $row) {
print_r($row);
}

上述例子一次只会为csv文件的一行分配内存。迭代大型数据集或者数列时最适合使用生成器,因为这样占用的系统内存量极少。

ModernPHP 系列全集:传送门

0%