PHP 5.5 中加入了生成器语法,但是我一直都很少使用,基本上和其它语言的实现类似,这里做一个简单的总结。

可迭代对象

PHP 中没有可迭代对象的概念,这里从 Python 中借过来用一下,所谓可迭代,可以简单的理解为可通过 for / foreach / while 等语法逐项读取的一个列表,比如说数组:

<?php
$colors = ['red', 'blue', 'white'];
for ($i = 0; $i < count($colors); $i ++) {
    var_dump($colors[$i]);
}

或者是一个仅包含 public 成员的普通类的对象:

<?php
class Student
{
    public $name;
    public $age;
    public $school;
}

$stu = new Student;
$stu->name   = "Tom";
$stu->age    = 23;
$stu->school = "Tsinghua";

// 当你迭代一个普通对象时,实际上调用的是这个对象的内部迭代器
foreach ($stu as $prop) {
	var_dump($prop);
}

或者是一个实现了 Iterator 接口的对象:

<?php
// 借用手册上 SPL 中的例子
$dir = new DirectoryIterator(dirname(__FILE__));
foreach ($dir as $fileinfo) {
    if (!$fileinfo->isDot()) {
        var_dump($fileinfo->getFilename());
    }
}

再有就是下面要提到的生成器。

生成器 Generator

从语法层面上来看,生成器很像一个函数,但是和函数不同的是,PHP 中的函数没有或者有且仅有一个返回值,而生成器则可能为一个循环生成 yield 多个值(每次一个,不是返回值,但很像)。

如果直接调用生成器,会返回一个可迭代的 Generator 对象,需要注意的是这时候生成器中的代码逻辑尚未执行。

<?php
function gen_demo() {
    echo "Hello, Generator";
	yield 1;
}
$g = gen_demo();
var_dump($g);
// object(Generator)#1 (0) {}

如果放入到一个迭代器中,则每次产生一个值:

<?php
function gen_demo() {
    yield 1;
    yield 2;
    yield 'verdana';
}
foreach (gen_demo() as $val) {
    var_dump($val);
}
// int(1)
// int(2)
// string(7) "verdana"

关于生成器的返回值,PHP5 中是没有的,如果加上 return 语句会报错。PHP7 中则加入了返回值,当迭代遇到 return 时,生成器会立刻终止,如果需要取得这个返回值,需要调用 Generator::getReturn() 方法。

<?php
function gen_demo() {
	yield 1;
	yield 2;
	return 3;
	yield 'verdana'; // never yield
}

$g = gen_demo();
foreach ($g as $num) {
	print $num . PHP_EOL;
}
// 1
// 2

print $g->getReturn() . PHP_EOL;
// 3

yield 关键字

类似 return 语句,但是 return 会立即终止执行剩下的代码;而 yield 则是暂停执行。

yield 也可以产生一个键值对(pair),比如:

<?php
function gen_demo() {
    yield 'name' => 'verdana';
}
foreach (gen_demo() as $k => $v) {
    print $k . ' -> ' . $v . PHP_EOL;
}
// name -> verdana

另外手册中提到了 yield from 语法,可以将一个 Generator 嵌入到另一个 Generator 中,有点类似 Trait 的意味。

使用场景?

比如说当我们需要迭代一个巨大的数据集合时,使用一个 Generator 而不是一个巨大的数组,可以帮我们节省大量的内存。

以一个巨大文本文件为例,如果我们使用 file_get_contents 将文件全部读入数组,肯定会超出脚本的内存限制。

当然在 PHP 中要以低内存消耗处理一个巨大的文件,有很多种方法,最简单的是利用 fopen/fgets 一行行读取并处理:

<?php
function do_something($line) {
    sleep(0.1);
}

$fp = fopen('file.txt', 'r');
if ($fp) {
    while (($line = fgets($fp, 1024)) != false) {
        do_something($line);
    }
    fclose($fp);
}

这样读取文件和处理代码必然混杂在一起,如果我们要分离处理代码,那么必须要先预读数据。我们看看如何使用 yield 来实现类似 file_get_contents 这样的操作。

<?php
function yield_get_contents($file) {
    echo round(memory_get_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
    $fp = fopen($file, 'r');
    if ($fp) {
        $i = 0;
        while (($line = fgets($fp, 1024)) != false) {
            yield $line;
            if (($i % 10000000) == 0) {
                echo round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL;
            }
            $i++;
        }
        fclose($fp);
    }
}

// Iterator and process
foreach (yield_get_contents('file.txt') as $line) {
    // your process code
}

以下是处理一个 4.5G 约 8000w 行的文本文件的内存消耗:

0.37 MB
0.38 MB
0.38 MB
0.38 MB
0.38 MB
0.38 MB
0.38 MB
0.38 MB
0.38 MB

总结:简单的说,当我们遇到数据量巨大且需要迭代的情况下,就可以考虑使用生成器语法,当然具体如何使用仍然需要结合实际的业务需求。