PHP 生成器

(PHP 5 >= 5.5.0, PHP 7, PHP 8)

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


生成器总览

生成器允许在 foreach 中迭代一组数据,而不会使内存达到上限,或者会消耗大量的处理时间。

写一个生成器函数,就像一个普通的自定义函数一样,和普通函数只返回一次不同的是,生成器可以通过 yield 多次返回需要的值。

一个简单的例子就是使用生成器来重新实现 range() 函数,标准的 range() 函数需要在内存中生成一个包含所有值的数组,然后返回该数组,结果就会占用很大的内存。 比如,调用 range(0, 1000000) 将导致内存占用超过 100 MB。

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

示例 将 range() 实现为生成器

<?php
function xrange($start, $limit, $step = 1) {
    if ($start <= $limit) {
        if ($step <= 0) {
            throw new LogicException('Step must be positive');
        }
        for ($i = $start; $i <= $limit; $i += $step) {
            yield $i;
        }
    } else {
        if ($step >= 0) {
            throw new LogicException('Step must be negative');
        }
        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

Generator 对象

调用生成器函数时会返回 Generator 类的一个对象。 该对象实现了 Iterator 接口,基本上和仅向前的迭代器一样, 它提供的方法可以操控生成器的状态,包括发送值、返回值。


生成器语法

生成器函数看起来像普通函数,不同的是普通函数返回一个值,而生成器可以 yield 生成多个想要的值。

任何包含 yield 的函数都是一个生成器函数。

当一个生成器被调用的时候,它返回一个可以被遍历的对象。当你遍历这个对象的时候(例如通过一个foreach循环),PHP 将会在每次需要值的时候调用对象的遍历方法,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。

一旦不再需要产生更多的值,生成器可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。

注意:生成器能够返回多个值,通过 Generator::getReturn() 可以获取到。

yield关键字

生成器函数的核心是yield关键字,它最简单的调用形式看起来像一个return申明,不同之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。

示例 一个简单的生成值的例子

<?php
function gen_one_to_three() {
    for ($i = 1; $i <= 3; $i++) {
        //注意变量$i的值在不同的yield之间是保持传递的。
        yield $i;
    }
}
$generator = gen_one_to_three();
foreach ($generator as $value) {
    echo "$value\n";
}

以上例程会输出:

1
2
3

注意:在内部会为生成的值配对连续的整型索引,就像一个非关联的数组。

警告:传入 Generator::send() 的值会被赋值到 $data, 或者直接调用 Generator::next() 时,赋的值将是 null。


生成器与 Iterator 对象的比较

生成器最主要的优点是简洁,和实现一个 Iterator 类相较而言, 同样的功能,用生成器可以编写更少的代码,可读性也更强。 

举例,下面的类和函数是相等的:

<?php
function getLinesFromFile($fileName) {
    if (!$fileHandle = fopen($fileName, 'r')) {
        return;
    }
 
    while (false !== $line = fgets($fileHandle)) {
        yield $line;
    }
 
    fclose($fileHandle);
}
// 比较下...
class LineIterator implements Iterator {
    protected $fileHandle;
 
    protected $line;
    protected $i;
 
    public function __construct($fileName) {
        if (!$this->fileHandle = fopen($fileName, 'r')) {
            throw new RuntimeException('Couldn\'t open file "' . $fileName . '"');
        }
    }
 
    public function rewind() {
        fseek($this->fileHandle, 0);
        $this->line = fgets($this->fileHandle);
        $this->i = 0;
    }
 
    public function valid() {
        return false !== $this->line;
    }
 
    public function current() {
        return $this->line;
    }
 
    public function key() {
        return $this->i;
    }
 
    public function next() {
        if (false !== $this->line) {
            $this->line = fgets($this->fileHandle);
            $this->i++;
        }
    }
 
    public function __destruct() {
        fclose($this->fileHandle);
    }
}

不过,这也付出了灵活性的代价: 生成器是一个只能向前的迭代器,一旦开始遍历就无法后退。